0
0
Fork 0
This commit is contained in:
Xeltica 2022-01-28 20:41:42 +09:00
parent 28769e798e
commit f5ee1bd943
17 changed files with 159 additions and 21 deletions

View file

@ -1,4 +1,18 @@
## 2.0 ## 2.2.0
* 全権限を許可するトークンへ更新
* トークン更新を促すように
* ログイン中のユーザーを明示するように
* トークンがおかしい場合、登録しているアカウントが他にあればそれを使うように
* デザイン調整
## 2.1.0
* 「ねこみみアジャスター」機能を追加
* デザイン調整
* セキュリティアップデート
## 2.0.0
* デザイン面・機能面での大幅な作り直し * デザイン面・機能面での大幅な作り直し

View file

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from 'typeorm';
export class addTokenVersion1643366857976 implements MigrationInterface {
name = 'addTokenVersion1643366857976'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "user" ADD "tokenVersion" integer NOT NULL DEFAULT 1');
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "tokenVersion"');
}
}

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey-tools", "name": "misskey-tools",
"version": "2.1.0", "version": "2.2.0",
"description": "", "description": "",
"main": "built/app.js", "main": "built/app.js",
"author": "Xeltica", "author": "Xeltica",

View file

@ -1 +1,50 @@
export const defaultTemplate = '昨日のMisskeyの活動は\n\nート: {notesCount}({notesDelta})\nフォロー : {followingCount}({followingDelta})\nフォロワー :{followersCount}({followersDelta})\n\nでした。\n{url}'; export const defaultTemplate = '昨日のMisskeyの活動は\n\nート: {notesCount}({notesDelta})\nフォロー : {followingCount}({followingDelta})\nフォロワー :{followersCount}({followersDelta})\n\nでした。\n{url}';
/**
* Misskeyアプリトークンバージョン
* ver 2:
* * 使
* * Misskey Toolsに
* ver 1:
* *
*/
export const currentTokenVersion = 2;
export const misskeyAppInfo = {
name: 'Misskey Tools',
description: 'A Professional Toolkit Designed for Misskey.',
permission: [
'read:account',
'write:account',
'read:blocks',
'write:blocks',
'read:drive',
'write:drive',
'read:favorites',
'write:favorites',
'read:following',
'write:following',
'read:messaging',
'write:messaging',
'read:mutes',
'write:mutes',
'write:notes',
'read:notifications',
'write:notifications',
'read:reactions',
'write:reactions',
'write:votes',
'read:pages',
'write:pages',
'write:page-likes',
'read:page-likes',
'read:user-groups',
'write:user-groups',
'read:channels',
'write:channels',
'read:gallery',
'write:gallery',
'read:gallery-likes',
'write:gallery-likes',
],
} as const;

View file

@ -3,13 +3,19 @@
* @author Xeltica * @author Xeltica
*/ */
import { readFile } from 'fs';
import { Get, JsonController } from 'routing-controllers'; import { Get, JsonController } from 'routing-controllers';
import { promisify } from 'util';
import { Meta } from '../../common/types/meta';
import { currentTokenVersion } from '../const';
@JsonController('/meta') @JsonController('/meta')
export class MetaController { export class MetaController {
@Get() get() { @Get() async get(): Promise<Meta> {
const {version} = JSON.parse(await promisify(readFile)(__dirname + '/../../meta.json', { encoding: 'utf-8'}));
return { return {
honi: 'ほに', version,
currentTokenVersion,
}; };
} }
} }

View file

@ -4,6 +4,7 @@ import { DeepPartial } from 'typeorm';
import { genToken } from './gen-token'; import { genToken } from './gen-token';
import { IUser } from '../../common/types/user'; import { IUser } from '../../common/types/user';
import { config } from '../../config'; import { config } from '../../config';
import { currentTokenVersion } from '../const';
/** /**
* IUser * IUser
@ -61,9 +62,9 @@ export const getUserByToolsToken = (token: string): Promise<IUser | undefined> =
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) {
await Users.update(u.id, { token }); await Users.update(u.id, { token, tokenVersion: currentTokenVersion });
} else { } else {
await Users.insert({ username, host, token }); await Users.insert({ username, host, token, tokenVersion: currentTokenVersion });
} }
}; };

View file

@ -98,4 +98,11 @@ export class User implements IUser {
default: false, default: false,
}) })
public bannedFromRanking: boolean; public bannedFromRanking: boolean;
@Column({
type: 'integer',
default: 1,
comment: 'Misskey API トークンのバージョン。現行と違う場合はアップデートを要求する',
})
public tokenVersion: number;
} }

View file

@ -10,6 +10,7 @@ import { config } from '../config';
import { upsertUser, getUser, updateUser, updateUsersToolsToken } from './functions/users'; import { upsertUser, getUser, updateUser, updateUsersToolsToken } from './functions/users';
import { api } from './services/misskey'; import { api } from './services/misskey';
import { die } from './die'; import { die } from './die';
import { misskeyAppInfo } from './const';
export const router = new Router<DefaultState, Context>(); export const router = new Router<DefaultState, Context>();
@ -36,9 +37,8 @@ router.get('/login', async ctx => {
// ホスト名の正規化 // ホスト名の正規化
host = meta.uri.replace(/^https?:\/\//, ''); host = meta.uri.replace(/^https?:\/\//, '');
const name = 'みす廃あらーと';
const description = 'ついついノートしすぎていませんか?'; const { name, permission, description } = misskeyAppInfo;
const permission = ['write:notes', 'write:notifications', 'write:drive', 'read:account', 'write:account'];
if (meta.features.miauth) { if (meta.features.miauth) {
// MiAuthを使用する // MiAuthを使用する

4
src/common/types/meta.ts Normal file
View file

@ -0,0 +1,4 @@
export interface Meta {
version: string;
currentTokenVersion: number;
}

View file

@ -19,5 +19,6 @@ export interface IUser {
rating: number; rating: number;
bannedFromRanking: boolean; bannedFromRanking: boolean;
isAdmin?: boolean; isAdmin?: boolean;
tokenVersion: number;
} }

View file

@ -13,7 +13,7 @@ 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 { getBrowserLanguage, resources } from './langs';
import { LOCALSTORAGE_KEY_LANG } from './const'; import { LOCALSTORAGE_KEY_LANG, XELTICA_STUDIO_URL } 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';
@ -46,7 +46,7 @@ const AppInner : React.VFC = () => {
return error ? ( return error ? (
<div className="container"> <div className="container">
<Header hasTopLink /> <Header hasTopLink className="xarticle mb-2" />
<div className="xarticle"> <div className="xarticle">
<h1>{t('error')}</h1> <h1>{t('error')}</h1>
<p>{t('_error.sorry')}</p> <p>{t('_error.sorry')}</p>
@ -67,7 +67,7 @@ const AppInner : React.VFC = () => {
<Route exact path="/announcements/:id" component={AnnouncementPage} /> <Route exact path="/announcements/:id" component={AnnouncementPage} />
</Switch> </Switch>
<footer className="text-center pa-5"> <footer className="text-center pa-5">
<p>(C)2020-2022 Xeltica Studio</p> <p>(C)2020-2022 <a href={XELTICA_STUDIO_URL} target="_blank" rel="noopener noreferrer">Xeltica Studio</a></p>
<p dangerouslySetInnerHTML={{__html: t('disclaimerForMisskeyHq')}} /> <p dangerouslySetInnerHTML={{__html: t('disclaimerForMisskeyHq')}} />
<p><Link to="/term">{t('termsOfService')}</Link></p> <p><Link to="/term">{t('termsOfService')}</Link></p>
</footer> </footer>

View file

@ -2,6 +2,8 @@ import React, { HTMLProps } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGetMetaQuery } from '../services/session';
import { CHANGELOG_URL } from '../const';
export type HeaderProps = { export type HeaderProps = {
hasTopLink?: boolean; hasTopLink?: boolean;
@ -12,12 +14,18 @@ export type HeaderProps = {
const messageNumber = Math.floor(Math.random() * 6) + 1; const messageNumber = Math.floor(Math.random() * 6) + 1;
export const Header: React.FC<HeaderProps> = ({hasTopLink, children, className, style}) => { export const Header: React.FC<HeaderProps> = ({hasTopLink, children, className, style}) => {
const {data: meta} = useGetMetaQuery(undefined);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<header className={`card ${className ?? ''}`} style={style}> <header className={`card ${className ?? ''}`} style={style}>
<div className="body"> <div className="body">
<h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}> <h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}>
{hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')} {hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')}
{meta && (
<a href={CHANGELOG_URL} target="_blank" rel="noopener noreferrer" className="text-125 text-dimmed ml-1">
v{meta?.version}
</a>
)}
</h1> </h1>
<h2 className="text-dimmed ml-1">{t(`_welcomeMessage.pattern${messageNumber}`)}</h2> <h2 className="text-dimmed ml-1">{t(`_welcomeMessage.pattern${messageNumber}`)}</h2>
{children} {children}

View file

@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { alertModes } from '../../common/types/alert-mode'; 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 { LOCALSTORAGE_KEY_TOKEN } from '../const'; import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { $post, $put } from '../misc/api'; import { $post, $put } from '../misc/api';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session'; import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { showModal } from '../store/slices/screen'; import { showModal } from '../store/slices/screen';
@ -169,6 +169,13 @@ export const MisshaiPage: React.VFC = () => {
if (session.error) { if (session.error) {
console.error(session.error); console.error(session.error);
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN); localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
const a = localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS);
if (a) {
const accounts = JSON.parse(a) as string[];
if (accounts.length > 0) {
localStorage.setItem(LOCALSTORAGE_KEY_TOKEN, accounts[0]);
}
}
location.reload(); location.reload();
} }
}, [session.error]); }, [session.error]);

View file

@ -4,3 +4,6 @@ export const LOCALSTORAGE_KEY_LANG = 'lang';
export const LOCALSTORAGE_KEY_ACCOUNTS = 'accounts'; export const LOCALSTORAGE_KEY_ACCOUNTS = 'accounts';
export const API_ENDPOINT = `//${location.host}/api/v1/`; export const API_ENDPOINT = `//${location.host}/api/v1/`;
export const CHANGELOG_URL = 'https://github.com/Xeltica/MisskeyTools/blob/master/CHANGELOG.md';
export const XELTICA_STUDIO_URL = 'https://xeltica.work';

View file

@ -45,6 +45,8 @@
"upload": "アップロード", "upload": "アップロード",
"preview": "プレビュー", "preview": "プレビュー",
"catAdjuster": "ねこみみアジャスター", "catAdjuster": "ねこみみアジャスター",
"shouldUpdateToken": "認証トークンの更新が必要です。更新しないと一部機能が正常に動作しません。",
"update": "更新する",
"_welcomeMessage": { "_welcomeMessage": {
"pattern1": "ついついノートしすぎていませんか?", "pattern1": "ついついノートしすぎていませんか?",
"pattern2": "Misskey, しすぎていませんか?", "pattern2": "Misskey, しすぎていませんか?",

View file

@ -10,16 +10,19 @@ import { useDispatch } from 'react-redux';
import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const'; import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
import { IUser } from '../../common/types/user'; import { IUser } from '../../common/types/user';
import { setAccounts } from '../store/slices/screen'; import { setAccounts } from '../store/slices/screen';
import { useGetSessionQuery } from '../services/session'; import { useGetMetaQuery, useGetSessionQuery } from '../services/session';
import { AdminPage } from '../components/AdminPage'; import { AdminPage } from '../components/AdminPage';
import { $get } from '../misc/api'; import { $get } from '../misc/api';
import { NekomimiPage } from '../components/NekomimiPage'; import { NekomimiPage } from '../components/NekomimiPage';
import { Card } from '../components/Card';
import { CurrentUser } from '../components/CurrentUser';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const [selectedTab, setSelectedTab] = useState<string>('misshai'); const [selectedTab, setSelectedTab] = useState<string>('misshai');
const {t, i18n} = useTranslation(); const {t, i18n} = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { data } = useGetSessionQuery(undefined); const { data: session } = useGetSessionQuery(undefined);
const { data: meta } = useGetMetaQuery(undefined);
useEffect(() => { useEffect(() => {
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[]; const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
@ -31,12 +34,12 @@ export const IndexSessionPage: React.VFC = () => {
it.push({ label: t('_nav.misshai'), key: 'misshai' }); it.push({ label: t('_nav.misshai'), key: 'misshai' });
it.push({ label: t('_nav.accounts'), key: 'accounts' }); it.push({ label: t('_nav.accounts'), key: 'accounts' });
it.push({ label: t('_nav.catAdjuster'), key: 'nekomimi', isNew: true }); it.push({ label: t('_nav.catAdjuster'), key: 'nekomimi', isNew: true });
if (data?.isAdmin) { if (session?.isAdmin) {
it.push({ label: 'Admin', key: 'admin' }); it.push({ label: 'Admin', key: 'admin' });
} }
it.push({ label: t('_nav.settings'), key: 'settings' }); it.push({ label: t('_nav.settings'), key: 'settings' });
return it; return it;
}, [i18n.language, data]); }, [i18n.language, session]);
const component = useMemo(() => { const component = useMemo(() => {
switch (selectedTab) { switch (selectedTab) {
@ -57,7 +60,16 @@ export const IndexSessionPage: React.VFC = () => {
<Tab items={items} selected={selectedTab} onSelect={setSelectedTab}/> <Tab items={items} selected={selectedTab} onSelect={setSelectedTab}/>
</div> </div>
</div> </div>
<article className="xarticle mt-4"> <article className="xarticle mt-2">
<Card className="mb-2">
<CurrentUser/>
{session && meta && meta.currentTokenVersion !== session.tokenVersion && (
<div className="text-danger mt-1">
{t('shouldUpdateToken')}
<a href={`/login?host=${encodeURIComponent(session.host)}`}>{t('update')}</a>
</div>
)}
</Card>
{component} {component}
</article> </article>
</> </>

View file

@ -2,14 +2,15 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const'; import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { IUser } from '../../common/types/user'; import { IUser } from '../../common/types/user';
import { Score } from '../../common/types/score'; import { Score } from '../../common/types/score';
import { Meta } from '../../common/types/meta';
export const sessionApi = createApi({ export const sessionApi = createApi({
reducerPath: 'session', reducerPath: 'session',
baseQuery: fetchBaseQuery({ baseUrl: API_ENDPOINT + 'session' }), baseQuery: fetchBaseQuery({ baseUrl: API_ENDPOINT }),
endpoints: (builder) => ({ endpoints: (builder) => ({
getSession: builder.query<IUser, undefined>({ getSession: builder.query<IUser, undefined>({
query: () => ({ query: () => ({
url: '/', url: '/session/',
headers: { headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`, 'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
} }
@ -17,7 +18,15 @@ export const sessionApi = createApi({
}), }),
getScore: builder.query<Score, undefined>({ getScore: builder.query<Score, undefined>({
query: () => ({ query: () => ({
url: '/score', url: '/session/score',
headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
}
})
}),
getMeta: builder.query<Meta, undefined>({
query: () => ({
url: '/meta',
headers: { headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`, 'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
} }
@ -29,4 +38,5 @@ export const sessionApi = createApi({
export const { export const {
useGetSessionQuery, useGetSessionQuery,
useGetScoreQuery, useGetScoreQuery,
useGetMetaQuery,
} = sessionApi; } = sessionApi;