0
0
Fork 0

wip: ログ

This commit is contained in:
Xeltica 2023-02-24 17:40:05 +09:00
parent d4c3673315
commit 0c3df4245d
9 changed files with 133 additions and 19 deletions

View file

@ -1,6 +1,26 @@
import { User } from '../models/entities/user'; import { User } from '../models/entities/user';
import { toSignedString } from '../../common/functions/to-signed-string'; import { toSignedString } from '../../common/functions/to-signed-string';
import {Count} from '../models/count'; 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<Score> => {
// TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも
const miUser = await api<MiUser>(user.host, 'users/show', { username: user.username }, user.token);
return {
notesCount: miUser.notesCount,
followingCount: miUser.followingCount,
followersCount: miUser.followersCount,
...getDelta(user, miUser),
};
};
/** /**
* *

View file

@ -9,7 +9,7 @@ const RETRY_COUNT = 5;
/** /**
* Misskey APIを呼び出す * Misskey APIを呼び出す
*/ */
export const api = async <T = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, token?: string): Promise<T> => { export const api = async <T extends Record<string, unknown> = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, token?: string): Promise<T> => {
const a = { ...arg }; const a = { ...arg };
if (token) { if (token) {
a.i = token; a.i = token;

View file

@ -1,5 +1,30 @@
import { User } from '../models/entities/user'; import { User } from '../models/entities/user';
import { api } from './misskey'; 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;
}
};
/** /**
* *

5
src/common/types/log.ts Normal file
View file

@ -0,0 +1,5 @@
export type Log = {
text: string;
level: 'error' | 'warn' | 'info';
timestamp: Date;
}

View file

@ -6,11 +6,13 @@ import { useTranslation } from 'react-i18next';
import { store } from './store'; import { store } from './store';
import { ModalComponent } from './Modal'; import { ModalComponent } from './Modal';
import { useTheme } from './misc/theme'; import { useTheme } from './misc/theme';
import { BREAKPOINT_SM } from './const'; import {BREAKPOINT_SM, LOCALSTORAGE_KEY_ACCOUNTS} from './const';
import { useGetSessionQuery } from './services/session'; import { useGetSessionQuery } from './services/session';
import { Router } from './Router'; import { Router } from './Router';
import { setMobile } from './store/slices/screen'; import {setAccounts, setMobile} from './store/slices/screen';
import { GeneralLayout } from './GeneralLayout'; import { GeneralLayout } from './GeneralLayout';
import {$get} from './misc/api';
import {IUser} from '../common/types/user';
const AppInner : React.VFC = () => { const AppInner : React.VFC = () => {
const { data: session } = useGetSessionQuery(undefined); const { data: session } = useGetSessionQuery(undefined);
@ -35,6 +37,11 @@ const AppInner : React.VFC = () => {
} }
}, [$location]); }, [$location]);
useEffect(() => {
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
}, [dispatch]);
useEffect(() => { useEffect(() => {
const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`); const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`);
const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches)); const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches));

View file

@ -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 (
<div className={`log ${log.level}`}>
[{time}] {log.text}
</div>
);
};
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 (
<>
<label className="input-check">
<input type="checkbox" checked={isVisibleInfo} onChange={e => setVisibleInfo(e.target.checked)} />
<span><i className="fas fa-circle-info fa-fw" /> INFO</span>
</label>
<label className="input-check">
<input type="checkbox" checked={isVisibleWarn} onChange={e => setVisibleWarn(e.target.checked)} />
<span><i className="fas fa-circle-exclamation fa-fw" /> WARN</span>
</label>
<label className="input-check">
<input type="checkbox" checked={isVisibleError} onChange={e => setVisibleError(e.target.checked)} />
<span><i className="fas fa-circle-xmark fa-fw" /> ERROR</span>
</label>
<div className="log-view vstack slim">
{filteredLog.map(l => <LogItem log={l} key={l.text} />)}
</div>
</>
);
};

View file

@ -8,6 +8,8 @@ import { $delete, $get, $post, $put } from '../misc/api';
import { showModal } from '../store/slices/screen'; import { showModal } from '../store/slices/screen';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useTitle } from '../hooks/useTitle'; import { useTitle } from '../hooks/useTitle';
import {Log} from '../../common/types/log';
import {LogView} from '../components/LogView';
export const AdminPage: React.VFC = () => { export const AdminPage: React.VFC = () => {
@ -25,7 +27,7 @@ export const AdminPage: React.VFC = () => {
const [draftTitle, setDraftTitle] = useState(''); const [draftTitle, setDraftTitle] = useState('');
const [draftBody, setDraftBody] = useState(''); const [draftBody, setDraftBody] = useState('');
const [misshaiLog, setMisshaiLog] = useState<string[] | null>(null); const [misshaiLog, setMisshaiLog] = useState<Log[] | null>(null);
const submitAnnouncement = async () => { const submitAnnouncement = async () => {
if (selectedAnnouncement) { if (selectedAnnouncement) {
@ -64,7 +66,7 @@ export const AdminPage: React.VFC = () => {
}; };
const fetchLog = () => { const fetchLog = () => {
$get<string[]>('admin/misshai/log').then(setMisshaiLog); $get<Log[]>('admin/misshai/log').then(setMisshaiLog);
}; };
const onClickStartMisshaiAlertWorkerButton = () => { const onClickStartMisshaiAlertWorkerButton = () => {
@ -163,7 +165,7 @@ export const AdminPage: React.VFC = () => {
))} ))}
{!isDeleteMode && ( {!isDeleteMode && (
<button className="item fluid" onClick={() => setEditMode(true)}> <button className="item fluid" onClick={() => setEditMode(true)}>
<i className="icon fas fa-plus"/ > <i className="icon fas fa-plus" />
Create New Create New
</button> </button>
)} )}
@ -200,7 +202,7 @@ export const AdminPage: React.VFC = () => {
</button> </button>
<h3></h3> <h3></h3>
<pre><code>{misshaiLog?.join('\n') ?? 'なし'}</code></pre> {misshaiLog && <LogView log={misshaiLog} />}
</div> </div>
</> </>
) )

View file

@ -1,28 +1,17 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; 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 { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { $get } from '../misc/api';
import { useAnnouncements } from '../hooks/useAnnouncements'; import { useAnnouncements } from '../hooks/useAnnouncements';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const {t} = useTranslation(); const {t} = useTranslation();
const dispatch = useDispatch();
const { data: session } = useGetSessionQuery(undefined); const { data: session } = useGetSessionQuery(undefined);
const score = useGetScoreQuery(undefined); const score = useGetScoreQuery(undefined);
const announcements = useAnnouncements(); const announcements = useAnnouncements();
useEffect(() => {
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
}, [dispatch]);
return ( return (
<article className="fade"> <article className="fade">
<section> <section>

View file

@ -166,4 +166,20 @@ small {
height: 1em; height: 1em;
width: 1em; width: 1em;
vertical-align: -0.1em; vertical-align: -0.1em;
}
.log-view {
background-color: var(--black);
}
.log {
&.info {
color: var(--skyblue);
}
&.error {
color: var(--red);
}
&.warn {
color: var(--yellow);
}
} }