0
0
Fork 0
This commit is contained in:
Xeltica 2021-09-24 02:36:56 +09:00
commit efea6b72e6
14 changed files with 157 additions and 203 deletions

View file

@ -1,6 +1,7 @@
import { Context } from 'koa'; import { Context } from 'koa';
import { ErrorCode } from '../common/types/error-code';
export const die = (ctx: Context, error = '問題が発生しました。お手数ですが、最初からやり直してください。', status = 400): Promise<void> => { export const die = (ctx: Context, error: ErrorCode = 'other', status = 400): Promise<void> => {
ctx.status = status; ctx.status = status;
return ctx.render('error', { error }); return ctx.render('frontend', { error });
}; };

View file

@ -7,13 +7,8 @@ import { v4 as uuid } from 'uuid';
import ms from 'ms'; import ms from 'ms';
import { config } from '../config'; import { config } from '../config';
import { upsertUser, getUser, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from './functions/users'; import { upsertUser, getUser, updateUser, updateUsersMisshaiToken } from './functions/users';
import { api } from './services/misskey'; import { api } from './services/misskey';
import { AlertMode, alertModes } from '../common/types/alert-mode';
import { Users } from './models';
import { sendAlert } from './services/send-alert';
import { visibilities, Visibility } from '../common/types/visibility';
import { defaultTemplate } from '../common/default-template';
import { die } from './die'; import { die } from './die';
export const router = new Router<DefaultState, Context>(); export const router = new Router<DefaultState, Context>();
@ -23,15 +18,14 @@ const tokenSecretCache: Record<string, string> = {};
router.get('/login', async ctx => { router.get('/login', async ctx => {
let host = ctx.query.host as string | undefined; let host = ctx.query.host as string | undefined;
if (!host) { if (!host) {
await die(ctx, 'host is empty'); await die(ctx, 'invalidParamater');
return; return;
} }
const meta = await api<{ name: string, uri: string, version: string, features: Record<string, boolean | undefined> }>(host, 'meta', {});
const meta = await api<{ name: string, uri: string, version: string, features: Record<string, boolean | undefined> }>(host, 'meta', {});
if (meta.version.includes('hitori')) { if (meta.version.includes('hitori')) {
await die(ctx, 'ひとりすきーは連携できません。'); await die(ctx, 'hitorisskeyIsDenied');
return; return;
} }
@ -42,7 +36,7 @@ router.get('/login', async ctx => {
const permission = ['write:notes', 'write:notifications']; const permission = ['write:notes', 'write:notifications'];
if (meta.features.miauth) { if (meta.features.miauth) {
// Use MiAuth // MiAuthを使用する
const callback = encodeURI(`${config.url}/miauth`); const callback = encodeURI(`${config.url}/miauth`);
const session = uuid(); const session = uuid();
@ -51,7 +45,7 @@ router.get('/login', async ctx => {
ctx.redirect(url); ctx.redirect(url);
} else { } else {
// Use legacy authentication // 旧型認証を使用する
const callbackUrl = encodeURI(`${config.url}/legacy-auth`); const callbackUrl = encodeURI(`${config.url}/legacy-auth`);
const { secret } = await api<{ secret: string }>(host, 'app/create', { const { secret } = await api<{ secret: string }>(host, 'app/create', {
@ -70,13 +64,13 @@ router.get('/login', async ctx => {
}); });
router.get('/teapot', async ctx => { router.get('/teapot', async ctx => {
await die(ctx, 'I\'m a teapot', 418); await die(ctx, 'teapot', 418);
}); });
router.get('/miauth', async ctx => { router.get('/miauth', async ctx => {
const session = ctx.query.session as string | undefined; const session = ctx.query.session as string | undefined;
if (!session) { if (!session) {
await die(ctx, 'session required'); await die(ctx, 'sessionRequired');
return; return;
} }
const host = sessionHostCache[session]; const host = sessionHostCache[session];
@ -101,7 +95,7 @@ router.get('/miauth', async ctx => {
router.get('/legacy-auth', async ctx => { router.get('/legacy-auth', async ctx => {
const token = ctx.query.token as string | undefined; const token = ctx.query.token as string | undefined;
if (!token) { if (!token) {
await die(ctx, 'token required'); await die(ctx, 'tokenRequired');
return; return;
} }
const host = sessionHostCache[token]; const host = sessionHostCache[token];
@ -125,97 +119,6 @@ router.get('/legacy-auth', async ctx => {
await login(ctx, user, host, i); await login(ctx, user, host, i);
}); });
router.post('/update-settings', async ctx => {
const mode = ctx.request.body.alertMode as AlertMode;
// 一応型チェック
if (!alertModes.includes(mode)) {
await die(ctx, `${mode} is an invalid value`);
return;
}
const visibility = ctx.request.body.visibility as Visibility;
// 一応型チェック
if (!visibilities.includes(visibility)) {
await die(ctx, `${mode} is an invalid value`);
return;
}
const flag = ctx.request.body.flag;
const template = ctx.request.body.template?.trim();
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
}
const u = await getUserByMisshaiToken(token);
if (!u) {
await die(ctx);
return;
}
await Users.update(u.id, {
alertMode: mode,
localOnly: flag === 'localOnly',
remoteFollowersOnly: flag === 'remoteFollowersOnly',
template: template === defaultTemplate || !template ? null : template,
visibility,
});
ctx.redirect('/?from=updateSettings');
});
router.post('/logout', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
}
ctx.cookies.set('token', '');
ctx.redirect('/?from=logout');
});
router.post('/optout', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
}
ctx.cookies.set('token', '');
const u = await getUserByMisshaiToken(token);
if (!u) {
await die(ctx);
return;
}
await deleteUser(u.username, u.host);
ctx.redirect('/?from=optout');
});
router.post('/send', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
}
const u = await getUserByMisshaiToken(token);
if (!u) {
await die(ctx);
return;
}
await sendAlert(u).catch(() => die(ctx));
ctx.redirect('/?from=send');
});
router.get('/assets/(.*)', async ctx => { router.get('/assets/(.*)', async ctx => {
await koaSend(ctx as any, ctx.path.replace('/assets/', ''), { await koaSend(ctx as any, ctx.path.replace('/assets/', ''), {
root: `${__dirname}/../assets/`, root: `${__dirname}/../assets/`,
@ -252,8 +155,5 @@ async function login(ctx: Context, user: Record<string, unknown>, host: string,
const misshaiToken = await updateUsersMisshaiToken(u); const misshaiToken = await updateUsersMisshaiToken(u);
ctx.cookies.set('token', misshaiToken, { signed: false, httpOnly: false }); await ctx.render('frontend', { token: misshaiToken });
// await ctx.render('logined', { user: u });
ctx.redirect('/');
} }

View file

@ -29,4 +29,13 @@ html
body body
#app: .loading Loading... #app: .loading Loading...
if token
script.
localStorage.setItem('token', '#{token}');
history.replaceState(null, null, '/');
if error
script.
window.__misshaialert = { error: '#{error}' };
script(src=`/assets/fe.${version}.js` async defer) script(src=`/assets/fe.${version}.js` async defer)

View file

@ -1,11 +0,0 @@
/**
* 稿
*/
export const defaultTemplate = `昨日のMisskeyの活動は
: {notesCount}({notesDelta})
: {followingCount}({followingDelta})
:{followersCount}({followersDelta})
{url}`;

View file

@ -0,0 +1,11 @@
export const errorCodes = [
'hitorisskeyIsDenied',
'teapot',
'sessionRequired',
'tokenRequired',
'invalidParamater',
'notAuthorized',
'other',
] as const;
export type ErrorCode = typeof errorCodes[number];

View file

@ -2,6 +2,8 @@ import React from 'react';
import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom'; import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { IndexPage } from './pages'; import { IndexPage } from './pages';
import { RankingPage } from './pages/ranking'; import { RankingPage } from './pages/ranking';
@ -10,10 +12,28 @@ import { TermPage } from './pages/term';
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 { getBrowserLanguage, resources } from './langs';
import { LOCALSTORAGE_KEY_LANG } from './const';
import 'xeltica-ui/dist/css/xeltica-ui.min.css'; import 'xeltica-ui/dist/css/xeltica-ui.min.css';
import './style.scss'; import './style.scss';
document.body.classList.add('dark');
if (!localStorage[LOCALSTORAGE_KEY_LANG]) {
localStorage[LOCALSTORAGE_KEY_LANG] = getBrowserLanguage();
}
i18n
.use(initReactI18next)
.init({
resources,
lng: localStorage[LOCALSTORAGE_KEY_LANG],
interpolation: {
escapeValue: false // react already safes from xss
}
});
const AppInner : React.VFC = () => { const AppInner : React.VFC = () => {
const $location = useLocation(); const $location = useLocation();
@ -21,8 +41,22 @@ const AppInner : React.VFC = () => {
const {t} = useTranslation(); const {t} = useTranslation();
return ( const error = (window as any).__misshaialert?.error;
<>
return error ? (
<div className="container">
<Header hasTopLink />
<div className="xarticle">
<h1>{t('error')}</h1>
<p>{t('_error.sorry')}</p>
<p>
{t('_error.additionalInfo')}
{t(`_error.${error}`)}
</p>
<Link to="/" onClick={() => (window as any).__misshaialert.error = null}>{t('retry')}</Link>
</div>
</div>
) : (
<div className="container"> <div className="container">
{$location.pathname !== '/' && <Header hasTopLink />} {$location.pathname !== '/' && <Header hasTopLink />}
<Switch> <Switch>
@ -36,7 +70,6 @@ const AppInner : React.VFC = () => {
</footer> </footer>
<ModalComponent /> <ModalComponent />
</div> </div>
</>
); );
}; };

View file

@ -35,7 +35,7 @@ export const SessionDataPage: React.VFC = () => {
<p>{t('welcomeBack', {acct: `@${session.data.username}@${session.data.host}`})}</p> <p>{t('welcomeBack', {acct: `@${session.data.username}@${session.data.host}`})}</p>
<p> <p>
<strong> <strong>
{t('_missHai.rating')}: {t('_missHai.rating')}{': '}
</strong> </strong>
{session.data.rating} {session.data.rating}
</p> </p>

View file

@ -3,7 +3,6 @@ import { alertModes } from '../../common/types/alert-mode';
import { IUser } from '../../common/types/user'; import { IUser } from '../../common/types/user';
import { Visibility } from '../../common/types/visibility'; import { Visibility } from '../../common/types/visibility';
import { useGetSessionQuery } from '../services/session'; import { useGetSessionQuery } from '../services/session';
import { defaultTemplate } from '../../common/default-template';
import { Card } from './Card'; import { Card } from './Card';
import { Theme, themes } from '../misc/theme'; import { Theme, themes } from '../misc/theme';
import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const'; import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
@ -127,7 +126,16 @@ export const SettingPage: React.VFC = () => {
const onClickLogout = useCallback(() => { const onClickLogout = useCallback(() => {
dispatch(showModal({ dispatch(showModal({
type: 'dialog', type: 'dialog',
message: 'WIP', title: 'ログアウトしてもよろしいですか?',
message: 'ログアウトしても、アラート送信や、お使いのMisskeyアカウントのデータ収集といった機能は動作し続けます。Misskey Toolsの利用を停止したい場合は、「アカウント連携を解除する」ボタンを押下して下さい。',
icon: 'question',
buttons: 'yesNo',
onSelect(i) {
if (i === 0) {
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
location.reload();
}
},
})); }));
}, [dispatch]); }, [dispatch]);
@ -138,6 +146,8 @@ export const SettingPage: React.VFC = () => {
})); }));
}, [dispatch]); }, [dispatch]);
const defaultTemplate = t('_template.default');
return session.isLoading || !data ? ( return session.isLoading || !data ? (
<div className="skeleton" style={{width: '100%', height: '128px'}}></div> <div className="skeleton" style={{width: '100%', height: '128px'}}></div>
) : ( ) : (
@ -155,16 +165,18 @@ export const SettingPage: React.VFC = () => {
</label> </label>
)) ))
} }
{draft.alertMode === 'notification' && ( </div>
{ draft.alertMode === 'notification' && (
<div className="alert bg-danger mt-2"> <div className="alert bg-danger mt-2">
<i className="icon bi bi-exclamation-circle"></i> <i className="icon bi bi-exclamation-circle"></i>
{t('_alertMode.notificationWarning')} {t('_alertMode.notificationWarning')}
</div> </div>
)} )}
</div>
{ draft.alertMode === 'note' && ( { draft.alertMode === 'note' && (
<>
<h2>{t('visibility')}</h2>
<div> <div>
<label htmlFor="visibility" className="input-field">{t('visibility')}</label>
{ {
availableVisibilities.map((visibility) => ( availableVisibilities.map((visibility) => (
<label key={visibility} className="input-check"> <label key={visibility} className="input-check">
@ -175,13 +187,14 @@ export const SettingPage: React.VFC = () => {
</label> </label>
)) ))
} }
</div>
<label className="input-check mt-2"> <label className="input-check mt-2">
<input type="checkbox" checked={draft.localOnly} onChange={(e) => { <input type="checkbox" checked={draft.localOnly} onChange={(e) => {
updateSetting({ localOnly: e.target.checked }); updateSetting({ localOnly: e.target.checked });
}} /> }} />
<span>{t('localOnly')}</span> <span>{t('localOnly')}</span>
</label> </label>
</div> </>
)} )}
</Card> </Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
@ -224,7 +237,7 @@ export const SettingPage: React.VFC = () => {
</ul> </ul>
</details> </details>
<div className="hstack" style={{justifyContent: 'flex-end'}}> <div className="hstack" style={{justifyContent: 'flex-end'}}>
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}></button> <button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button>
<button className="btn primary" onClick={() => { <button className="btn primary" onClick={() => {
updateSettingWithDialog({ template: draft.template === '' ? null : draft.template }); updateSettingWithDialog({ template: draft.template === '' ? null : draft.template });
}}>{t('save')}</button> }}>{t('save')}</button>

View file

@ -1,38 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { App } from './App'; import { App } from './App';
import { LOCALSTORAGE_KEY_TOKEN } from './const';
import { resources } from './langs';
document.body.classList.add('dark');
// cookieにトークンが入ってたらlocalStorageに移し替える
const token = document.cookie
.split('; ')
.find(row => row.startsWith('token'))
?.split('=')[1];
if (token) {
localStorage[LOCALSTORAGE_KEY_TOKEN] = token;
// 今の所はcookieをトークン以外に使用しないため全消去する
// もしcookieの用途が増えるのであればここを良い感じに書き直す必要がある
document.cookie = '';
}
console.log(resources);
i18n
.use(initReactI18next)
.init({
resources,
lng: localStorage['lang'] ?? 'ja_JP',
interpolation: {
escapeValue: false // react already safes from xss
}
});
ReactDOM.render(<App/>, document.getElementById('app')); ReactDOM.render(<App/>, document.getElementById('app'));

View file

@ -29,11 +29,14 @@
failedToFetch: "Failed to fetch", failedToFetch: "Failed to fetch",
registeredUsersCount: "Users", registeredUsersCount: "Users",
isCalculating: "It is being calculated now. Please check back later!", isCalculating: "It is being calculated now. Please check back later!",
retry: "Try again",
ok: "OK", ok: "OK",
yes: "Yes", yes: "Yes",
no: "No", no: "No",
termsOfService: "Terms of Service", termsOfService: "Terms of Service",
name: "Name", name: "Name",
resetToDefault: "Reset to default",
error: "Error",
_welcomeMessage: { _welcomeMessage: {
pattern1: "Overnoting Misskey?", pattern1: "Overnoting Misskey?",
pattern2: "Overusing Misskey?", pattern2: "Overusing Misskey?",
@ -92,8 +95,20 @@
system: "Follows System Preferences", system: "Follows System Preferences",
}, },
_template: { _template: {
description: "アラートの自動投稿をカスタマイズできます。", description: "Customize template of your alert.",
description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。", description2: "Hashtag '#misshaialert' will be appended regardless of the template.",
default: "My Misskey activity yesterday was:\n\nNotes: {notesCount}({notesDelta})\nFollowing: {followingCount}({followingDelta})\nFollowers: {followersCount}({followersDelta})\n\n{url}",
},
_error: {
sorry: "Something went wrong. Please retry again.",
additionalInfo: "Additional Info: ",
hitorisskeyIsDenied: "You cannot integrate with hitorisskey.",
teapot: "I'm a teapot.",
sessionRequired: "Session is required.",
tokenRequired: "Token is required.",
invalidParameter: "Invalid parameter.",
notAuthorized: "Not authorized.",
other: "None",
}, },
}, },
} }

View file

@ -9,4 +9,11 @@ export const resources = {
export const languageName = { export const languageName = {
'en_US': 'English', 'en_US': 'English',
'ja_JP': '日本語', 'ja_JP': '日本語',
} as const;
export type LanguageCode = keyof typeof resources;
export const getBrowserLanguage = () => {
const lang = navigator.language;
return (Object.keys(resources) as LanguageCode[]).find(k => k.startsWith(lang)) ?? 'en_US';
}; };

View file

@ -34,6 +34,9 @@
no: "いいえ", no: "いいえ",
termsOfService: "利用規約", termsOfService: "利用規約",
name: "名前", name: "名前",
resetToDefault: "初期値に戻す",
error: "エラー",
retry: "やり直す",
_welcomeMessage: { _welcomeMessage: {
'pattern1': 'ついついノートしすぎていませんか?', 'pattern1': 'ついついノートしすぎていませんか?',
'pattern2': 'Misskey, しすぎていませんか?', 'pattern2': 'Misskey, しすぎていませんか?',
@ -94,6 +97,18 @@
_template: { _template: {
description: "アラートの自動投稿をカスタマイズできます。", description: "アラートの自動投稿をカスタマイズできます。",
description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。", description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。",
default: "昨日のMisskeyの活動は\n\nート: {notesCount}({notesDelta})\nフォロー : {followingCount}({followingDelta})\nフォロワー :{followersCount}({followersDelta})\n\nでした。\n{url}",
},
_error: {
sorry: "問題が発生しました。お手数ですが、やり直してください。",
additionalInfo: "追加情報: ",
hitorisskeyIsDenied: "ひとりすきーは連携できません。",
teapot: "I'm a teapot.",
sessionRequired: "セッションがありません。",
tokenRequired: "トークンがありません。",
invalidParameter: "パラメータが不正です。",
notAuthorized: "権限がありません。",
other: "なし",
}, },
}, },
} }

View file

@ -1,8 +0,0 @@
export const welcomeMessage = [
'ついついノートしすぎていませんか?',
'Misskey, しすぎていませんか?',
'今日、何ノート書いた?',
'10000 ノートは初心者、そう思っていませんか?',
'息するように Misskey、そんなあなたへ。',
'あなたは真の Misskey 廃人ですか?',
];

View file

@ -7,6 +7,7 @@ import { SettingPage } from '../components/SettingPage';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RankingPage } from '../components/RankingPage'; import { RankingPage } from '../components/RankingPage';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const [selectedTab, setSelectedTab] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<number>(0);
const {t, i18n} = useTranslation(); const {t, i18n} = useTranslation();