コメント追加したり
This commit is contained in:
parent
1c45e759c3
commit
b9575d2c5b
22 changed files with 945 additions and 809 deletions
|
@ -19,6 +19,9 @@ module.exports = {
|
||||||
'indent': [
|
'indent': [
|
||||||
'error',
|
'error',
|
||||||
'tab',
|
'tab',
|
||||||
|
{
|
||||||
|
'SwitchCase': 1,
|
||||||
|
}
|
||||||
],
|
],
|
||||||
'quotes': [
|
'quotes': [
|
||||||
'error',
|
'error',
|
||||||
|
|
|
@ -15,6 +15,12 @@ export class RankingController {
|
||||||
return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined);
|
return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DBに問い合わせてランキングを取得する
|
||||||
|
* @param isCalculating 現在算出中かどうか
|
||||||
|
* @param limit 何件取得するか
|
||||||
|
* @returns ランキング
|
||||||
|
*/
|
||||||
private async getResponse(isCalculating: boolean, limit?: number) {
|
private async getResponse(isCalculating: boolean, limit?: number) {
|
||||||
const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({
|
const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
|
|
|
@ -2,6 +2,9 @@ import { config } from '../../config';
|
||||||
import { User } from '../models/entities/user';
|
import { User } from '../models/entities/user';
|
||||||
import { Score } from '../../common/types/score';
|
import { Score } from '../../common/types/score';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* デフォルトの投稿用テンプレート
|
||||||
|
*/
|
||||||
export const defaultTemplate = `昨日のMisskeyの活動は
|
export const defaultTemplate = `昨日のMisskeyの活動は
|
||||||
|
|
||||||
ノート: {notesCount}({notesDelta})
|
ノート: {notesCount}({notesDelta})
|
||||||
|
@ -11,11 +14,17 @@ export const defaultTemplate = `昨日のMisskeyの活動は
|
||||||
でした。
|
でした。
|
||||||
{url}`;
|
{url}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 埋め込み変数の型
|
||||||
|
*/
|
||||||
export type Variable = {
|
export type Variable = {
|
||||||
description?: string;
|
description?: string;
|
||||||
replace?: string | ((score: Score, user: User) => string);
|
replace?: string | ((score: Score, user: User) => string);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 埋め込み可能な変数のリスト
|
||||||
|
*/
|
||||||
export const variables: Record<string, Variable> = {
|
export const variables: Record<string, Variable> = {
|
||||||
notesCount: {
|
notesCount: {
|
||||||
description: 'ノート数',
|
description: 'ノート数',
|
||||||
|
@ -61,6 +70,12 @@ export const variables: Record<string, Variable> = {
|
||||||
|
|
||||||
const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g;
|
const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* スコア情報とユーザー情報からテキストを生成する
|
||||||
|
* @param score スコア情報
|
||||||
|
* @param user ユーザー情報
|
||||||
|
* @returns 生成したテキスト
|
||||||
|
*/
|
||||||
export const format = (score: Score, user: User): string => {
|
export const format = (score: Score, user: User): string => {
|
||||||
const template = user.template || defaultTemplate;
|
const template = user.template || defaultTemplate;
|
||||||
return template.replace(variableRegex, (m, name) => {
|
return template.replace(variableRegex, (m, name) => {
|
||||||
|
|
|
@ -2,6 +2,9 @@ import rndstr from 'rndstr';
|
||||||
import { UsedToken } from '../models/entities/used-token';
|
import { UsedToken } from '../models/entities/used-token';
|
||||||
import { UsedTokens } from '../models';
|
import { UsedTokens } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* トークンを生成します
|
||||||
|
*/
|
||||||
export const genToken = async (): Promise<string> => {
|
export const genToken = async (): Promise<string> => {
|
||||||
let used: UsedToken | undefined = undefined;
|
let used: UsedToken | undefined = undefined;
|
||||||
let token: string;
|
let token: string;
|
||||||
|
|
|
@ -3,6 +3,11 @@ import { Score } from '../../common/types/score';
|
||||||
import { api } from '../services/misskey';
|
import { api } from '../services/misskey';
|
||||||
import { toSignedString } from './to-signed-string';
|
import { toSignedString } from './to-signed-string';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザーのスコアを取得します。
|
||||||
|
* @param user ユーザー
|
||||||
|
* @returns ユーザーのスコア
|
||||||
|
*/
|
||||||
export const getScores = async (user: User): Promise<Score> => {
|
export const getScores = async (user: User): Promise<Score> => {
|
||||||
const miUser = await api<Record<string, number>>(user.host, 'users/show', { username: user.username }, user.token);
|
const miUser = await api<Record<string, number>>(user.host, 'users/show', { username: user.username }, user.token);
|
||||||
if (miUser.error) {
|
if (miUser.error) {
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { Users } from '../models';
|
import { Users } from '../models';
|
||||||
import { User } from '../models/entities/user';
|
import { User } from '../models/entities/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ミス廃ランキングを取得する
|
||||||
|
* @param limit 取得する件数
|
||||||
|
* @returns ミス廃ランキング
|
||||||
|
*/
|
||||||
export const getRanking = async (limit: number | null = 10): Promise<User[]> => {
|
export const getRanking = async (limit: number | null = 10): Promise<User[]> => {
|
||||||
const query = Users.createQueryBuilder('user')
|
const query = Users.createQueryBuilder('user')
|
||||||
.where('"user"."bannedFromRanking" IS NOT TRUE')
|
.where('"user"."bannedFromRanking" IS NOT TRUE')
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
|
/**
|
||||||
|
* 数値を符号付き数値の文字列に変換する
|
||||||
|
* @param num 数値
|
||||||
|
* @returns 符号付き数値の文字列
|
||||||
|
*/
|
||||||
export const toSignedString = (num: number): string => num < 0 ? num.toString() : '+' + num;
|
export const toSignedString = (num: number): string => num < 0 ? num.toString() : '+' + num;
|
||||||
|
|
|
@ -4,6 +4,11 @@ import { User } from '../models/entities/user';
|
||||||
import { updateUser } from './users';
|
import { updateUser } from './users';
|
||||||
import { MiUser } from './update-score';
|
import { MiUser } from './update-score';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザーのレーティングを更新します
|
||||||
|
* @param user ユーザー
|
||||||
|
* @param miUser Misskeyのユーザー
|
||||||
|
*/
|
||||||
export const updateRating = async (user: User, miUser: MiUser): Promise<void> => {
|
export const updateRating = async (user: User, miUser: MiUser): Promise<void> => {
|
||||||
const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1;
|
const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1;
|
||||||
await updateUser(user.username, user.host, {
|
await updateUser(user.username, user.host, {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { User } from '../models/entities/user';
|
import { User } from '../models/entities/user';
|
||||||
import { updateUser } from './users';
|
import { updateUser } from './users';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misskeyのユーザーモデル
|
||||||
|
*/
|
||||||
export type MiUser = {
|
export type MiUser = {
|
||||||
notesCount: number,
|
notesCount: number,
|
||||||
followingCount: number,
|
followingCount: number,
|
||||||
|
@ -8,6 +11,11 @@ export type MiUser = {
|
||||||
createdAt: string,
|
createdAt: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* スコアを更新します
|
||||||
|
* @param user ユーザー
|
||||||
|
* @param miUser Misskeyのユーザー
|
||||||
|
*/
|
||||||
export const updateScore = async (user: User, miUser: MiUser): Promise<void> => {
|
export const updateScore = async (user: User, miUser: MiUser): Promise<void> => {
|
||||||
await updateUser(user.username, user.host, {
|
await updateUser(user.username, user.host, {
|
||||||
prevNotesCount: miUser.notesCount ?? 0,
|
prevNotesCount: miUser.notesCount ?? 0,
|
||||||
|
|
|
@ -2,12 +2,22 @@ import { User } from '../models/entities/user';
|
||||||
import { Users } from '../models';
|
import { Users } from '../models';
|
||||||
import { DeepPartial } from 'typeorm';
|
import { DeepPartial } from 'typeorm';
|
||||||
import { genToken } from './gen-token';
|
import { genToken } from './gen-token';
|
||||||
import pick from 'object.pick';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザーを取得します
|
||||||
|
* @param username ユーザー名
|
||||||
|
* @param host ホスト名
|
||||||
|
* @returns ユーザー
|
||||||
|
*/
|
||||||
export const getUser = (username: string, host: string): Promise<User | undefined> => {
|
export const getUser = (username: string, host: string): Promise<User | undefined> => {
|
||||||
return Users.findOne({ username, host });
|
return Users.findOne({ username, host });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザーのミス廃トークンを更新します。
|
||||||
|
* @param user ユーザー
|
||||||
|
* @returns ミス廃トークン
|
||||||
|
*/
|
||||||
export const updateUsersMisshaiToken = async (user: User | User['id']): Promise<string> => {
|
export const updateUsersMisshaiToken = async (user: User | User['id']): Promise<string> => {
|
||||||
const u = typeof user === 'number'
|
const u = typeof user === 'number'
|
||||||
? user
|
? user
|
||||||
|
@ -18,10 +28,21 @@ export const updateUsersMisshaiToken = async (user: User | User['id']): Promise<
|
||||||
return misshaiToken;
|
return misshaiToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ミス廃トークンからユーザーを取得します。
|
||||||
|
* @param token ミス廃トークン
|
||||||
|
* @returns ユーザー
|
||||||
|
*/
|
||||||
export const getUserByMisshaiToken = (token: string): Promise<User | undefined> => {
|
export const getUserByMisshaiToken = (token: string): Promise<User | undefined> => {
|
||||||
return Users.findOne({ misshaiToken: token });
|
return Users.findOne({ misshaiToken: token });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザー情報を更新するか新規作成します。
|
||||||
|
* @param username ユーザー名
|
||||||
|
* @param host ホスト名
|
||||||
|
* @param token トークン
|
||||||
|
*/
|
||||||
export const upsertUser = async (username: string, host: string, token: string): Promise<void> => {
|
export const upsertUser = async (username: string, host: string, token: string): Promise<void> => {
|
||||||
const u = await getUser(username, host);
|
const u = await getUser(username, host);
|
||||||
if (u) {
|
if (u) {
|
||||||
|
@ -31,14 +52,29 @@ export const upsertUser = async (username: string, host: string, token: string):
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザー情報を更新します。
|
||||||
|
* @param username ユーザー名
|
||||||
|
* @param host ホスト名
|
||||||
|
* @param record 既存のユーザー情報
|
||||||
|
*/
|
||||||
export const updateUser = async (username: string, host: string, record: DeepPartial<User>): Promise<void> => {
|
export const updateUser = async (username: string, host: string, record: DeepPartial<User>): Promise<void> => {
|
||||||
await Users.update({ username, host }, record);
|
await Users.update({ username, host }, record);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定したユーザーを削除します。
|
||||||
|
* @param username ユーザー名
|
||||||
|
* @param host ホスト名
|
||||||
|
*/
|
||||||
export const deleteUser = async (username: string, host: string): Promise<void> => {
|
export const deleteUser = async (username: string, host: string): Promise<void> => {
|
||||||
await Users.delete({ username, host });
|
await Users.delete({ username, host });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザー数を取得します。
|
||||||
|
* @returns ユーザー数
|
||||||
|
*/
|
||||||
export const getUserCount = (): Promise<number> => {
|
export const getUserCount = (): Promise<number> => {
|
||||||
return Users.count();
|
return Users.count();
|
||||||
};
|
};
|
|
@ -11,7 +11,7 @@ import { upsertUser, getUser, updateUser, updateUsersMisshaiToken, getUserByMiss
|
||||||
import { api } from './services/misskey';
|
import { api } from './services/misskey';
|
||||||
import { AlertMode, alertModes } from '../common/types/alert-mode';
|
import { AlertMode, alertModes } from '../common/types/alert-mode';
|
||||||
import { Users } from './models';
|
import { Users } from './models';
|
||||||
import { send } from './services/send';
|
import { sendAlert } from './services/send-alert';
|
||||||
import { visibilities, Visibility } from '../common/types/visibility';
|
import { visibilities, Visibility } from '../common/types/visibility';
|
||||||
import { defaultTemplate } from './functions/format';
|
import { defaultTemplate } from './functions/format';
|
||||||
import { die } from './die';
|
import { die } from './die';
|
||||||
|
@ -212,7 +212,7 @@ router.post('/send', async ctx => {
|
||||||
await die(ctx);
|
await die(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await send(u).catch(() => die(ctx));
|
await sendAlert(u).catch(() => die(ctx));
|
||||||
ctx.redirect('/?from=send');
|
ctx.redirect('/?from=send');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,13 @@ export const entities = [
|
||||||
UsedToken,
|
UsedToken,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データベース接続が既に存在すれば取得し、なければ新規作成する
|
||||||
|
* @param force 既存の接続があっても新規作成するかどうか
|
||||||
|
* @returns 取得または作成したDBコネクション
|
||||||
|
*/
|
||||||
export const initDb = async (force = false): Promise<Connection> => {
|
export const initDb = async (force = false): Promise<Connection> => {
|
||||||
|
// forceがtrueでない限り、既に接続が存在する場合はそれを返す
|
||||||
if (!force) {
|
if (!force) {
|
||||||
try {
|
try {
|
||||||
const conn = getConnection();
|
const conn = getConnection();
|
||||||
|
@ -19,6 +25,7 @@ export const initDb = async (force = false): Promise<Connection> => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 接続がないか、forceがtrueの場合は新規作成する
|
||||||
return createConnection({
|
return createConnection({
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: config.db.host,
|
host: config.db.host,
|
||||||
|
|
|
@ -4,9 +4,11 @@ import _const from '../const';
|
||||||
export const ua = `Mozilla/5.0 misshaialertBot/${_const.version} +https://github.com/Xeltica/misshaialert Node/${process.version}`;
|
export const ua = `Mozilla/5.0 misshaialertBot/${_const.version} +https://github.com/Xeltica/misshaialert Node/${process.version}`;
|
||||||
|
|
||||||
axios.defaults.headers['User-Agent'] = ua;
|
axios.defaults.headers['User-Agent'] = ua;
|
||||||
|
|
||||||
axios.defaults.validateStatus = (stat) => stat < 500;
|
axios.defaults.validateStatus = (stat) => stat < 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misskey APIを呼び出す
|
||||||
|
*/
|
||||||
export const api = <T = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, i?: string): Promise<T> => {
|
export const api = <T = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, i?: string): Promise<T> => {
|
||||||
const a = { ...arg };
|
const a = { ...arg };
|
||||||
if (i) {
|
if (i) {
|
||||||
|
@ -15,6 +17,12 @@ export const api = <T = Record<string, unknown>>(host: string, endpoint: string,
|
||||||
return axios.post<T>(`https://${host}/api/${endpoint}`, a).then(res => res.data);
|
return axios.post<T>(`https://${host}/api/${endpoint}`, a).then(res => res.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* トークンが有効かどうかを確認する
|
||||||
|
* @param host 対象のホスト名
|
||||||
|
* @param i トークン
|
||||||
|
* @returns トークンが有効ならtrue、無効ならfalse
|
||||||
|
*/
|
||||||
export const apiAvailable = async (host: string, i: string): Promise<boolean> => {
|
export const apiAvailable = async (host: string, i: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const res = await api(host, 'i', {}, i);
|
const res = await api(host, 'i', {}, i);
|
||||||
|
|
55
src/backend/services/send-alert.ts
Normal file
55
src/backend/services/send-alert.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { User } from '../models/entities/user';
|
||||||
|
import { format } from '../functions/format';
|
||||||
|
import { getScores } from '../functions/get-scores';
|
||||||
|
import { api } from './misskey';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アラートを送信する
|
||||||
|
* @param user ユーザー
|
||||||
|
*/
|
||||||
|
export const sendAlert = async (user: User) => {
|
||||||
|
const text = format(await getScores(user), user);
|
||||||
|
switch (user.alertMode) {
|
||||||
|
case 'note':
|
||||||
|
await sendNoteAlert(text, user);
|
||||||
|
break;
|
||||||
|
case 'notification':
|
||||||
|
await sendNotificationAlert(text, user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ノートアラートを送信する
|
||||||
|
* @param text 通知内容
|
||||||
|
* @param user ユーザー
|
||||||
|
*/
|
||||||
|
const sendNoteAlert = async (text: string, user: User) => {
|
||||||
|
const res = await api<Record<string, unknown>>(user.host, 'notes/create', {
|
||||||
|
text,
|
||||||
|
visibility: user.visibility,
|
||||||
|
localOnly: user.localOnly,
|
||||||
|
remoteFollowersOnly: user.remoteFollowersOnly,
|
||||||
|
}, user.token);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
throw res.error || res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知アラートを送信する
|
||||||
|
* @param text 通知内容
|
||||||
|
* @param user ユーザー
|
||||||
|
*/
|
||||||
|
const sendNotificationAlert = async (text: string, user: User) => {
|
||||||
|
const res = await api(user.host, 'notifications/create', {
|
||||||
|
header: 'みす廃あらーと',
|
||||||
|
icon: 'https://i.imgur.com/B991yTl.png',
|
||||||
|
body: text,
|
||||||
|
}, user.token);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
throw res.error || res;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,34 +0,0 @@
|
||||||
import { User } from '../models/entities/user';
|
|
||||||
import { format } from '../functions/format';
|
|
||||||
import { getScores } from '../functions/get-scores';
|
|
||||||
import { api } from './misskey';
|
|
||||||
|
|
||||||
export const send = async (user: User): Promise<void> => {
|
|
||||||
const text = format(await getScores(user), user);
|
|
||||||
|
|
||||||
if (user.alertMode === 'note') {
|
|
||||||
console.info(`send ${user.username}@${user.host}'s misshaialert as a note`);
|
|
||||||
const opts = {
|
|
||||||
text,
|
|
||||||
visibility: user.visibility,
|
|
||||||
} as Record<string, unknown>;
|
|
||||||
if (user.localOnly) opts.localOnly = user.localOnly;
|
|
||||||
if (user.remoteFollowersOnly) opts.remoteFollowersOnly = user.remoteFollowersOnly;
|
|
||||||
const res = await api<Record<string, unknown>>(user.host, 'notes/create', opts, user.token);
|
|
||||||
if (res.error) {
|
|
||||||
throw res.error || res;
|
|
||||||
}
|
|
||||||
} else if (user.alertMode === 'notification') {
|
|
||||||
console.info(`send ${user.username}@${user.host}'s misshaialert as a notification`);
|
|
||||||
const res = await api(user.host, 'notifications/create', {
|
|
||||||
header: 'みす廃あらーと',
|
|
||||||
icon: 'https://i.imgur.com/B991yTl.png',
|
|
||||||
body: text,
|
|
||||||
}, user.token);
|
|
||||||
if (res.error) {
|
|
||||||
throw res.error || res;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.info(`will not send ${user.username}@${user.host}'s misshaialert`);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -7,30 +7,48 @@ import { MiUser, updateScore } from '../functions/update-score';
|
||||||
import { updateRating } from '../functions/update-rating';
|
import { updateRating } from '../functions/update-rating';
|
||||||
import { AlertMode } from '../../common/types/alert-mode';
|
import { AlertMode } from '../../common/types/alert-mode';
|
||||||
import { Users } from '../models';
|
import { Users } from '../models';
|
||||||
import { send } from './send';
|
import { sendAlert } from './send-alert';
|
||||||
import { api } from './misskey';
|
import { api } from './misskey';
|
||||||
import * as Store from '../store';
|
import * as Store from '../store';
|
||||||
|
import { User } from '../models/entities/user';
|
||||||
|
|
||||||
export default (): void => {
|
export default (): void => {
|
||||||
cron.schedule('0 0 0 * * *', async () => {
|
cron.schedule('0 0 0 * * *', async () => {
|
||||||
Store.dispatch({
|
Store.dispatch({ nowCalculating: true });
|
||||||
nowCalculating: true,
|
|
||||||
});
|
const users = await Users.find({ alertMode: Not<AlertMode>('nothing') });
|
||||||
const users = await Users.find({
|
|
||||||
alertMode: Not<AlertMode>('nothing'),
|
|
||||||
});
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
try {
|
await update(user).catch(e => handleError(user, e));
|
||||||
|
|
||||||
|
if (user.alertMode === 'note') {
|
||||||
|
return delay(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Store.dispatch({ nowCalculating: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アラートを送信します。
|
||||||
|
* @param user アラートの送信先ユーザー
|
||||||
|
*/
|
||||||
|
const update = async (user: User) => {
|
||||||
const miUser = await api<MiUser & { error: unknown }>(user.host, 'users/show', { username: user.username }, user.token);
|
const miUser = await api<MiUser & { error: unknown }>(user.host, 'users/show', { username: user.username }, user.token);
|
||||||
if (miUser.error) throw miUser.error;
|
if (miUser.error) throw miUser.error;
|
||||||
|
|
||||||
await updateRating(user, miUser);
|
await updateRating(user, miUser);
|
||||||
await send(user);
|
await sendAlert(user);
|
||||||
|
|
||||||
await updateScore(user, miUser);
|
await updateScore(user, miUser);
|
||||||
|
};
|
||||||
|
|
||||||
if (user.alertMode === 'note')
|
/**
|
||||||
await delay(3000);
|
* アラート送信失敗のエラーをハンドリングします。
|
||||||
} catch (e: any) {
|
* @param user 送信に失敗したアラートの送信先ユーザー
|
||||||
|
* @param e エラー。ErrorだったりObjectだったりするのでanyだけど、いずれ型定義したい
|
||||||
|
*/
|
||||||
|
const handleError = async (user: User, e: any) => {
|
||||||
if (e.code) {
|
if (e.code) {
|
||||||
if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') {
|
if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') {
|
||||||
// ユーザーが削除されている場合、レコードからも消してとりやめ
|
// ユーザーが削除されている場合、レコードからも消してとりやめ
|
||||||
|
@ -43,10 +61,4 @@ export default (): void => {
|
||||||
// おそらく通信エラー
|
// おそらく通信エラー
|
||||||
console.error(`Unknown error: ${e.name} ${e.message}`);
|
console.error(`Unknown error: ${e.name} ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
Store.dispatch({
|
|
||||||
nowCalculating: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,18 +2,32 @@
|
||||||
// getStateを介してステートを取得し、dispatchによって更新する
|
// getStateを介してステートを取得し、dispatchによって更新する
|
||||||
// stateを直接編集できないようになっている
|
// stateを直接編集できないようになっている
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初期値
|
||||||
|
*/
|
||||||
const defaultState: State = {
|
const defaultState: State = {
|
||||||
nowCalculating: false,
|
nowCalculating: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _state: Readonly<State> = defaultState;
|
let _state: Readonly<State> = defaultState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ステートの型
|
||||||
|
*/
|
||||||
export type State = {
|
export type State = {
|
||||||
nowCalculating: boolean,
|
nowCalculating: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getState = () => Object.freeze({..._state});
|
/**
|
||||||
|
* 現在のステートを読み取り専用な形式で取得します。
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getState = () => Object.freeze({ ..._state });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ステートを更新します。
|
||||||
|
* @param mutation ステートの一部を更新するためのオブジェクト
|
||||||
|
*/
|
||||||
export const dispatch = (mutation: Partial<State>) => {
|
export const dispatch = (mutation: Partial<State>) => {
|
||||||
_state = {
|
_state = {
|
||||||
..._state,
|
..._state,
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-do
|
||||||
import { IndexPage } from './pages';
|
import { IndexPage } from './pages';
|
||||||
import { RankingPage } from './pages/ranking';
|
import { RankingPage } from './pages/ranking';
|
||||||
import { Header } from './components/Header';
|
import { Header } from './components/Header';
|
||||||
|
import { TermPage } from './pages/term';
|
||||||
|
|
||||||
import 'xeltica-ui/dist/css/xeltica-ui.min.css';
|
import 'xeltica-ui/dist/css/xeltica-ui.min.css';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import { TermPage } from './pages/term';
|
|
||||||
|
|
||||||
const AppInner : React.VFC = () => {
|
const AppInner : React.VFC = () => {
|
||||||
const $location = useLocation();
|
const $location = useLocation();
|
||||||
|
|
30
src/frontend/components/Tab.tsx
Normal file
30
src/frontend/components/Tab.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
export type TabItem = {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TabProps = {
|
||||||
|
items: TabItem[];
|
||||||
|
selected: number;
|
||||||
|
onSelect: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// タブコンポーネント
|
||||||
|
export const Tab: React.FC<TabProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div className="tab">
|
||||||
|
{props.items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={'item ' + (index === props.selected ? 'selected' : '')}
|
||||||
|
onClick={() => props.onSelect(index)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -7,3 +7,27 @@ body {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--margin);
|
||||||
|
&.active {
|
||||||
|
color: var(--primary);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 0;
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue