diff --git a/src/backend/functions/get-scores.ts b/src/backend/functions/get-scores.ts index 70adf25..739c66d 100644 --- a/src/backend/functions/get-scores.ts +++ b/src/backend/functions/get-scores.ts @@ -1,6 +1,26 @@ import { User } from '../models/entities/user'; import { toSignedString } from '../../common/functions/to-signed-string'; import {Count} from '../models/count'; +import {api} from '../services/misskey'; +import {Score} from '../../common/types/score'; +import {MiUser} from './update-score'; + +/** + * ユーザーのスコアを取得します。 + * @param user ユーザー + * @returns ユーザーのスコア + */ +export const getScores = async (user: User): Promise => { + // TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも + const miUser = await api(user.host, 'users/show', { username: user.username }, user.token); + + return { + notesCount: miUser.notesCount, + followingCount: miUser.followingCount, + followersCount: miUser.followersCount, + ...getDelta(user, miUser), + }; +}; /** * ユーザーのスコア差分を取得します。 diff --git a/src/backend/services/misskey.ts b/src/backend/services/misskey.ts index 3fe6e71..775bb0b 100644 --- a/src/backend/services/misskey.ts +++ b/src/backend/services/misskey.ts @@ -9,7 +9,7 @@ const RETRY_COUNT = 5; /** * Misskey APIを呼び出す */ -export const api = async >(host: string, endpoint: string, arg: Record, token?: string): Promise => { +export const api = async = Record>(host: string, endpoint: string, arg: Record, token?: string): Promise => { const a = { ...arg }; if (token) { a.i = token; diff --git a/src/backend/services/send-alert.ts b/src/backend/services/send-alert.ts index d313278..e1cc472 100644 --- a/src/backend/services/send-alert.ts +++ b/src/backend/services/send-alert.ts @@ -1,5 +1,30 @@ import { User } from '../models/entities/user'; import { api } from './misskey'; +import {format} from '../../common/functions/format'; +import {getScores} from '../functions/get-scores'; + + +/** + * アラートを送信する + * @param user ユーザー + */ +export const sendAlert = async (user: User) => { + const text = format(user, await getScores(user)); + switch (user.alertMode) { + case 'note': + await sendNoteAlert(text, user); + break; + case 'notification': + await sendNotificationAlert(text, user); + break; + case 'both': + await Promise.all([ + sendNotificationAlert(text, user), + sendNoteAlert(text, user), + ]); + break; + } +}; /** * ノートアラートを送信する diff --git a/src/common/types/log.ts b/src/common/types/log.ts new file mode 100644 index 0000000..a119392 --- /dev/null +++ b/src/common/types/log.ts @@ -0,0 +1,5 @@ +export type Log = { + text: string; + level: 'error' | 'warn' | 'info'; + timestamp: Date; +} diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 813d1cc..7c99ef6 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -6,11 +6,13 @@ import { useTranslation } from 'react-i18next'; import { store } from './store'; import { ModalComponent } from './Modal'; import { useTheme } from './misc/theme'; -import { BREAKPOINT_SM } from './const'; +import {BREAKPOINT_SM, LOCALSTORAGE_KEY_ACCOUNTS} from './const'; import { useGetSessionQuery } from './services/session'; import { Router } from './Router'; -import { setMobile } from './store/slices/screen'; +import {setAccounts, setMobile} from './store/slices/screen'; import { GeneralLayout } from './GeneralLayout'; +import {$get} from './misc/api'; +import {IUser} from '../common/types/user'; const AppInner : React.VFC = () => { const { data: session } = useGetSessionQuery(undefined); @@ -35,6 +37,11 @@ const AppInner : React.VFC = () => { } }, [$location]); + useEffect(() => { + const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[]; + Promise.all(accounts.map(token => $get('session', token))).then(a => dispatch(setAccounts(a as IUser[]))); + }, [dispatch]); + useEffect(() => { const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`); const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches)); diff --git a/src/frontend/components/LogView.tsx b/src/frontend/components/LogView.tsx new file mode 100644 index 0000000..9a41a57 --- /dev/null +++ b/src/frontend/components/LogView.tsx @@ -0,0 +1,50 @@ +import React, {useMemo, useState} from 'react'; +import {Log} from '../../common/types/log'; +import dayjs from 'dayjs'; + +const LogItem: React.FC<{log: Log}> = ({log}) => { + const time = dayjs(log.timestamp).format('hh:mm:ss'); + + return ( +
+ [{time}] {log.text} +
+ ); +}; + +export const LogView: React.FC<{log: Log[]}> = ({log}) => { + const [isVisibleInfo, setVisibleInfo] = useState(true); + const [isVisibleWarn, setVisibleWarn] = useState(true); + const [isVisibleError, setVisibleError] = useState(true); + + const filter = useMemo(() => { + const levels: Log['level'][] = []; + if (isVisibleError) levels.push('error'); + if (isVisibleWarn) levels.push('warn'); + if (isVisibleInfo) levels.push('info'); + + return levels; + }, [isVisibleError, isVisibleWarn, isVisibleInfo]); + + const filteredLog = useMemo(() => log.filter(l => filter.includes(l.level)), [log, filter]); + + return ( + <> + + + +
+ {filteredLog.map(l => )} +
+ + ); +}; diff --git a/src/frontend/pages/admin.tsx b/src/frontend/pages/admin.tsx index 8a3efa2..c20bf9b 100644 --- a/src/frontend/pages/admin.tsx +++ b/src/frontend/pages/admin.tsx @@ -8,6 +8,8 @@ import { $delete, $get, $post, $put } from '../misc/api'; import { showModal } from '../store/slices/screen'; import { useDispatch } from 'react-redux'; import { useTitle } from '../hooks/useTitle'; +import {Log} from '../../common/types/log'; +import {LogView} from '../components/LogView'; export const AdminPage: React.VFC = () => { @@ -25,7 +27,7 @@ export const AdminPage: React.VFC = () => { const [draftTitle, setDraftTitle] = useState(''); const [draftBody, setDraftBody] = useState(''); - const [misshaiLog, setMisshaiLog] = useState(null); + const [misshaiLog, setMisshaiLog] = useState(null); const submitAnnouncement = async () => { if (selectedAnnouncement) { @@ -64,7 +66,7 @@ export const AdminPage: React.VFC = () => { }; const fetchLog = () => { - $get('admin/misshai/log').then(setMisshaiLog); + $get('admin/misshai/log').then(setMisshaiLog); }; const onClickStartMisshaiAlertWorkerButton = () => { @@ -163,7 +165,7 @@ export const AdminPage: React.VFC = () => { ))} {!isDeleteMode && ( )} @@ -200,7 +202,7 @@ export const AdminPage: React.VFC = () => { ミス廃アラートワーカーを強制起動する

直近のワーカーエラー

-
{misshaiLog?.join('\n') ?? 'なし'}
+ {misshaiLog && } ) diff --git a/src/frontend/pages/index.session.tsx b/src/frontend/pages/index.session.tsx index d71efcb..8bc0080 100644 --- a/src/frontend/pages/index.session.tsx +++ b/src/frontend/pages/index.session.tsx @@ -1,28 +1,17 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const'; -import { IUser } from '../../common/types/user'; -import { setAccounts } from '../store/slices/screen'; import { useGetScoreQuery, useGetSessionQuery } from '../services/session'; -import { $get } from '../misc/api'; import { useAnnouncements } from '../hooks/useAnnouncements'; import { Link } from 'react-router-dom'; export const IndexSessionPage: React.VFC = () => { const {t} = useTranslation(); - const dispatch = useDispatch(); const { data: session } = useGetSessionQuery(undefined); const score = useGetScoreQuery(undefined); const announcements = useAnnouncements(); - useEffect(() => { - const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[]; - Promise.all(accounts.map(token => $get('session', token))).then(a => dispatch(setAccounts(a as IUser[]))); - }, [dispatch]); - return (
diff --git a/src/frontend/style.scss b/src/frontend/style.scss index d33cf54..99d90d2 100644 --- a/src/frontend/style.scss +++ b/src/frontend/style.scss @@ -166,4 +166,20 @@ small { height: 1em; width: 1em; vertical-align: -0.1em; +} + +.log-view { + background-color: var(--black); +} + +.log { + &.info { + color: var(--skyblue); + } + &.error { + color: var(--red); + } + &.warn { + color: var(--yellow); + } } \ No newline at end of file