From 69212dd99a572daaa6a24cac39adb48d1a38b022 Mon Sep 17 00:00:00 2001 From: Xeltica Date: Sat, 25 Feb 2023 17:13:07 +0900 Subject: [PATCH] =?UTF-8?q?ES=20Modules=E3=81=AB=E7=A7=BB=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js => .eslintrc.cjs | 11 +- .gitignore | 3 +- .idea/codeStyles/Project.xml | 9 +- .idea/jsLibraryMappings.xml | 6 + .yarnrc.yml | 2 +- build-meta.js | 6 +- migration/1596513280623-Init.ts | 18 +- migration/1596514165166-Init2.ts | 22 +- migration/1599570288522-mypage.ts | 22 +- migration/1599577510614-mode.ts | 18 +- migration/1609938844427-visibility.ts | 30 +- migration/1609941393782-template.ts | 14 +- migration/1609948116186-rating.ts | 22 +- migration/1651804009671-useRanking.ts | 16 +- ormconfig.js => ormconfig.cjs | 0 package.json | 24 +- src/app.ts | 10 +- src/backend/const.ts | 72 +- src/backend/controllers/admin.ts | 56 +- src/backend/controllers/announcement.ts | 166 +-- .../controllers/body/announce-create.ts | 4 +- .../controllers/body/announce-update.ts | 6 +- src/backend/controllers/body/id-prop.ts | 2 +- src/backend/controllers/body/user-setting.ts | 32 +- src/backend/controllers/index.ts | 13 + src/backend/controllers/meta.ts | 20 +- src/backend/controllers/ranking.ts | 52 +- src/backend/controllers/session.ts | 64 +- src/backend/die.ts | 6 +- src/backend/functions/error-to-string.ts | 10 +- src/backend/functions/gen-token.ts | 18 +- src/backend/functions/get-scores.ts | 38 +- src/backend/functions/ranking.ts | 22 +- src/backend/functions/update-rating.ts | 16 +- src/backend/functions/update-score.ts | 24 +- src/backend/functions/users.ts | 60 +- src/backend/models/count.ts | 6 +- src/backend/models/entities/announcement.ts | 46 +- src/backend/models/entities/used-token.ts | 10 +- src/backend/models/entities/user.ts | 182 +-- src/backend/models/index.ts | 6 +- src/backend/render.ts | 11 +- src/backend/router.ts | 256 ++-- src/backend/server.ts | 57 +- src/backend/services/db.ts | 56 +- src/backend/services/misskey.ts | 68 +- src/backend/services/send-alert.ts | 72 +- src/backend/services/worker.ts | 186 +-- src/backend/store.ts | 29 +- src/backend/utils/group-by.ts | 18 +- src/common/functions/create-gacha.ts | 30 +- src/common/functions/format.ts | 56 +- src/common/types/alert-mode.ts | 8 +- src/common/types/announcement.ts | 10 +- src/common/types/design-system-color.ts | 28 +- src/common/types/error-code.ts | 18 +- src/common/types/log.ts | 6 +- src/common/types/meta.ts | 4 +- src/common/types/score.ts | 12 +- src/common/types/user.ts | 42 +- src/common/types/visibility.ts | 8 +- src/config.ts | 10 + src/frontend/App.tsx | 118 +- src/frontend/GeneralLayout.tsx | 84 +- src/frontend/Header.tsx | 34 +- src/frontend/Modal.tsx | 154 +-- src/frontend/Router.tsx | 24 +- src/frontend/components/AnnouncementList.tsx | 44 +- src/frontend/components/Card.tsx | 14 +- src/frontend/components/CurrentUser.tsx | 8 +- src/frontend/components/HashtagTimeline.tsx | 16 +- src/frontend/components/LogView.tsx | 72 +- src/frontend/components/LoginForm.tsx | 66 +- src/frontend/components/NavigationMenu.tsx | 116 +- src/frontend/components/Ranking.tsx | 80 +- src/frontend/components/Skeleton.tsx | 6 +- src/frontend/components/Tab.tsx | 32 +- src/frontend/hooks/useAnnouncements.ts | 22 +- src/frontend/hooks/useTitle.ts | 14 +- src/frontend/init.tsx | 18 +- src/frontend/langs/index.ts | 26 +- src/frontend/misc/api.ts | 54 +- src/frontend/misc/theme.ts | 72 +- src/frontend/modal/dialog.ts | 28 +- src/frontend/modal/menu.ts | 18 +- src/frontend/modal/modal.ts | 4 +- src/frontend/pages/account.tsx | 98 +- src/frontend/pages/admin.tsx | 338 +++--- src/frontend/pages/announcement.tsx | 44 +- src/frontend/pages/announcements/index.tsx | 10 +- src/frontend/pages/apps/avatar-cropper.tsx | 254 ++-- src/frontend/pages/apps/misshai.tsx | 552 ++++----- src/frontend/pages/apps/misshai/ranking.tsx | 26 +- src/frontend/pages/index.session.tsx | 128 +- src/frontend/pages/index.tsx | 4 +- src/frontend/pages/index.welcome.tsx | 124 +- src/frontend/pages/settings.tsx | 276 ++--- src/frontend/services/session.ts | 63 +- src/frontend/store/index.ts | 12 +- src/frontend/store/slices/screen.ts | 106 +- src/tools/calculate-all-rating.ts | 6 +- src/tools/calculate-all-rating.worker.ts | 28 +- tsconfig.json | 5 +- webpack.config.js => webpack.config.cjs | 11 - yarn.lock | 1026 ++++++++--------- 105 files changed, 3154 insertions(+), 3230 deletions(-) rename .eslintrc.js => .eslintrc.cjs (72%) create mode 100644 .idea/jsLibraryMappings.xml rename ormconfig.js => ormconfig.cjs (100%) create mode 100644 src/backend/controllers/index.ts rename webpack.config.js => webpack.config.cjs (85%) diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 72% rename from .eslintrc.js rename to .eslintrc.cjs index 4847d34..f349ad8 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -16,13 +16,7 @@ module.exports = { '@typescript-eslint' ], 'rules': { - 'indent': [ - 'error', - 'tab', - { - 'SwitchCase': 1, - } - ], + 'indent': ['error', 2, { 'SwitchCase': 1 } ], 'quotes': [ 'error', 'single' @@ -31,6 +25,7 @@ module.exports = { 'error', 'always' ], - '@typescript-eslint/explicit-module-boundary-types': 'off' + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', } }; diff --git a/.gitignore b/.gitignore index 23517c8..77549eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules built yarn-error.log -config.json \ No newline at end of file +config.json +.yarn \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 715721a..fdc96d9 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,14 +1,19 @@ + diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml index e71de34..8e8345b 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,2 +1,2 @@ yarnPath: .yarn/releases/yarn-1.22.19.cjs -nodeLinker: node-modulesl \ No newline at end of file +nodeLinker: node-modules \ No newline at end of file diff --git a/build-meta.js b/build-meta.js index 99dd3a7..0c083d5 100644 --- a/build-meta.js +++ b/build-meta.js @@ -1,8 +1,8 @@ -const { readFileSync, writeFileSync } = require('fs'); +import {readFileSync, writeFileSync} from 'fs'; const { version } = JSON.parse(readFileSync('./package.json', { - encoding: 'UTF-8', - flag: 'r', + encoding: 'UTF-8', + flag: 'r', })); writeFileSync('built/meta.json', JSON.stringify({ version })); diff --git a/migration/1596513280623-Init.ts b/migration/1596513280623-Init.ts index 8ac2f34..bec421d 100644 --- a/migration/1596513280623-Init.ts +++ b/migration/1596513280623-Init.ts @@ -1,16 +1,16 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class Init1596513280623 implements MigrationInterface { - name = 'Init1596513280623' + name = 'Init1596513280623'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('CREATE TABLE "user" ("id" SERIAL NOT NULL, "username" character varying NOT NULL, "host" character varying NOT NULL, "token" character varying NOT NULL, "prevNotesCount" integer NOT NULL, "prevFollowingCount" integer NOT NULL, "prevFollowersCount" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))'); - await queryRunner.query('CREATE UNIQUE INDEX "IDX_6269eebacdb25de8569298a52a" ON "user" ("username", "host") '); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TABLE "user" ("id" SERIAL NOT NULL, "username" character varying NOT NULL, "host" character varying NOT NULL, "token" character varying NOT NULL, "prevNotesCount" integer NOT NULL, "prevFollowingCount" integer NOT NULL, "prevFollowersCount" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))'); + await queryRunner.query('CREATE UNIQUE INDEX "IDX_6269eebacdb25de8569298a52a" ON "user" ("username", "host") '); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP INDEX "IDX_6269eebacdb25de8569298a52a"'); - await queryRunner.query('DROP TABLE "user"'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX "IDX_6269eebacdb25de8569298a52a"'); + await queryRunner.query('DROP TABLE "user"'); + } } diff --git a/migration/1596514165166-Init2.ts b/migration/1596514165166-Init2.ts index 76e3873..7b8c8ad 100644 --- a/migration/1596514165166-Init2.ts +++ b/migration/1596514165166-Init2.ts @@ -1,18 +1,18 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class Init21596514165166 implements MigrationInterface { - name = 'Init21596514165166' + name = 'Init21596514165166'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevNotesCount" SET DEFAULT 0'); - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowingCount" SET DEFAULT 0'); - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowersCount" SET DEFAULT 0'); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevNotesCount" SET DEFAULT 0'); + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowingCount" SET DEFAULT 0'); + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowersCount" SET DEFAULT 0'); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowersCount" DROP DEFAULT'); - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowingCount" DROP DEFAULT'); - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevNotesCount" DROP DEFAULT'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowersCount" DROP DEFAULT'); + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevFollowingCount" DROP DEFAULT'); + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "prevNotesCount" DROP DEFAULT'); + } } diff --git a/migration/1599570288522-mypage.ts b/migration/1599570288522-mypage.ts index 9d58963..7722433 100644 --- a/migration/1599570288522-mypage.ts +++ b/migration/1599570288522-mypage.ts @@ -1,18 +1,18 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class mypage1599570288522 implements MigrationInterface { - name = 'mypage1599570288522' + name = 'mypage1599570288522'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('CREATE TABLE "used_token" ("token" character varying NOT NULL, CONSTRAINT "PK_7f2db4c33c33cd6b38e63393fe5" PRIMARY KEY ("token"))'); - await queryRunner.query('CREATE UNIQUE INDEX "IDX_7f2db4c33c33cd6b38e63393fe" ON "used_token" ("token") '); - await queryRunner.query('ALTER TABLE "user" ADD "misshaiToken" character varying NOT NULL DEFAULT \'\''); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TABLE "used_token" ("token" character varying NOT NULL, CONSTRAINT "PK_7f2db4c33c33cd6b38e63393fe5" PRIMARY KEY ("token"))'); + await queryRunner.query('CREATE UNIQUE INDEX "IDX_7f2db4c33c33cd6b38e63393fe" ON "used_token" ("token") '); + await queryRunner.query('ALTER TABLE "user" ADD "misshaiToken" character varying NOT NULL DEFAULT \'\''); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "misshaiToken"'); - await queryRunner.query('DROP INDEX "IDX_7f2db4c33c33cd6b38e63393fe"'); - await queryRunner.query('DROP TABLE "used_token"'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "misshaiToken"'); + await queryRunner.query('DROP INDEX "IDX_7f2db4c33c33cd6b38e63393fe"'); + await queryRunner.query('DROP TABLE "used_token"'); + } } diff --git a/migration/1599577510614-mode.ts b/migration/1599577510614-mode.ts index 29e2dfc..2fa66f5 100644 --- a/migration/1599577510614-mode.ts +++ b/migration/1599577510614-mode.ts @@ -1,16 +1,16 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class mode1599577510614 implements MigrationInterface { - name = 'mode1599577510614' + name = 'mode1599577510614'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('CREATE TYPE "user_alertmode_enum" AS ENUM(\'note\', \'notification\', \'nothing\')'); - await queryRunner.query('ALTER TABLE "user" ADD "alertMode" "user_alertmode_enum" NOT NULL DEFAULT \'note\''); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TYPE "user_alertmode_enum" AS ENUM(\'note\', \'notification\', \'nothing\')'); + await queryRunner.query('ALTER TABLE "user" ADD "alertMode" "user_alertmode_enum" NOT NULL DEFAULT \'note\''); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "alertMode"'); - await queryRunner.query('DROP TYPE "user_alertmode_enum"'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "alertMode"'); + await queryRunner.query('DROP TYPE "user_alertmode_enum"'); + } } diff --git a/migration/1609938844427-visibility.ts b/migration/1609938844427-visibility.ts index 86b5de2..fb52467 100644 --- a/migration/1609938844427-visibility.ts +++ b/migration/1609938844427-visibility.ts @@ -1,22 +1,22 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class visibility1609938844427 implements MigrationInterface { - name = 'visibility1609938844427' + name = 'visibility1609938844427'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('CREATE TYPE "user_visibility_enum" AS ENUM(\'public\', \'home\', \'followers\', \'users\')'); - await queryRunner.query('ALTER TABLE "user" ADD "visibility" "user_visibility_enum" NOT NULL DEFAULT \'home\''); - await queryRunner.query('ALTER TABLE "user" ADD "localOnly" boolean NOT NULL DEFAULT false'); - await queryRunner.query('ALTER TABLE "user" ADD "remoteFollowersOnly" boolean NOT NULL DEFAULT false'); - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" SET DEFAULT \'notification\''); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TYPE "user_visibility_enum" AS ENUM(\'public\', \'home\', \'followers\', \'users\')'); + await queryRunner.query('ALTER TABLE "user" ADD "visibility" "user_visibility_enum" NOT NULL DEFAULT \'home\''); + await queryRunner.query('ALTER TABLE "user" ADD "localOnly" boolean NOT NULL DEFAULT false'); + await queryRunner.query('ALTER TABLE "user" ADD "remoteFollowersOnly" boolean NOT NULL DEFAULT false'); + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" SET DEFAULT \'notification\''); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" SET DEFAULT \'note\''); - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "remoteFollowersOnly"'); - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "localOnly"'); - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "visibility"'); - await queryRunner.query('DROP TYPE "user_visibility_enum"'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" SET DEFAULT \'note\''); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "remoteFollowersOnly"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "localOnly"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "visibility"'); + await queryRunner.query('DROP TYPE "user_visibility_enum"'); + } } diff --git a/migration/1609941393782-template.ts b/migration/1609941393782-template.ts index 1533555..9785c7b 100644 --- a/migration/1609941393782-template.ts +++ b/migration/1609941393782-template.ts @@ -1,14 +1,14 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class template1609941393782 implements MigrationInterface { - name = 'template1609941393782' + name = 'template1609941393782'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" ADD "template" character varying(280)'); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ADD "template" character varying(280)'); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "template"'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "template"'); + } } diff --git a/migration/1609948116186-rating.ts b/migration/1609948116186-rating.ts index 2d03922..e5ada0d 100644 --- a/migration/1609948116186-rating.ts +++ b/migration/1609948116186-rating.ts @@ -1,18 +1,18 @@ import {MigrationInterface, QueryRunner} from 'typeorm'; export class rating1609948116186 implements MigrationInterface { - name = 'rating1609948116186' + name = 'rating1609948116186'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" ADD "prevRating" real NOT NULL DEFAULT 0'); - await queryRunner.query('ALTER TABLE "user" ADD "rating" real NOT NULL DEFAULT 0'); - await queryRunner.query('ALTER TABLE "user" ADD "bannedFromRanking" boolean NOT NULL DEFAULT false'); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ADD "prevRating" real NOT NULL DEFAULT 0'); + await queryRunner.query('ALTER TABLE "user" ADD "rating" real NOT NULL DEFAULT 0'); + await queryRunner.query('ALTER TABLE "user" ADD "bannedFromRanking" boolean NOT NULL DEFAULT false'); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "bannedFromRanking"'); - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "rating"'); - await queryRunner.query('ALTER TABLE "user" DROP COLUMN "prevRating"'); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "bannedFromRanking"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "rating"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "prevRating"'); + } } diff --git a/migration/1651804009671-useRanking.ts b/migration/1651804009671-useRanking.ts index 59a7e7f..b94452b 100644 --- a/migration/1651804009671-useRanking.ts +++ b/migration/1651804009671-useRanking.ts @@ -1,13 +1,13 @@ -import {MigrationInterface, QueryRunner} from "typeorm"; +import {MigrationInterface, QueryRunner} from 'typeorm'; export class useRanking1651804009671 implements MigrationInterface { - name = 'useRanking1651804009671' + name = 'useRanking1651804009671'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" ADD "useRanking" boolean NOT NULL DEFAULT false`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ADD "useRanking" boolean NOT NULL DEFAULT false'); + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "useRanking"`); - } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "useRanking"'); + } } diff --git a/ormconfig.js b/ormconfig.cjs similarity index 100% rename from ormconfig.js rename to ormconfig.cjs diff --git a/package.json b/package.json index 4b927dd..625731a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "built/app.js", "author": "Shrimpia Network", "private": true, + "type": "module", "scripts": { "build": "run-s build:backend build:frontend", "build:frontend": "webpack", @@ -30,13 +31,11 @@ "@koa/multer": "^3.0.2", "@reduxjs/toolkit": "^1.6.1", "axios": "^0.21.2", - "class-transformer": "^0.4.0", + "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "css-loader": "^6.2.0", "dayjs": "^1.10.7", "deepmerge": "^4.2.2", - "delay": "^4.4.0", - "fibers": "^5.0.0", "i18next": "^20.6.1", "i18next-browser-languagedetector": "^6.1.2", "insert-text-at-cursor": "^0.3.0", @@ -65,7 +64,7 @@ "react-twemoji": "^0.5.0", "reflect-metadata": "^0.1.13", "rndstr": "^1.0.0", - "routing-controllers": "^0.9.0", + "routing-controllers": "^0.10.1", "sass": "^1.38.2", "sass-loader": "^12.1.0", "striptags": "^3.2.0", @@ -75,7 +74,7 @@ "tsc-alias": "^1.3.9", "tsconfig-paths-webpack-plugin": "^3.5.1", "typeorm": "0.2.25", - "typescript": "^4.4.2", + "typescript": "^4.9.5", "uuid": "^8.3.0", "webpack": "^5.75.0", "webpack-cli": "^4.8.0", @@ -91,7 +90,7 @@ "@types/koa-views": "^2.0.4", "@types/markdown-it": "^12.2.3", "@types/ms": "^0.7.31", - "@types/node": "^8.0.29", + "@types/node": "^18.14.1", "@types/node-cron": "^2.0.3", "@types/object.pick": "^1.3.1", "@types/react": "^17.0.19", @@ -100,17 +99,14 @@ "@types/react-twemoji": "^0.4.0", "@types/styled-components": "^5.1.13", "@types/uuid": "^8.0.0", - "@typescript-eslint/eslint-plugin": "^4.30.0", - "@typescript-eslint/parser": "^4.30.0", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@typescript-eslint/parser": "^5.53.0", "copyfiles": "^2.3.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-prettier": "^3.1.4", + "eslint": "^8.34.0", "nodemon": "^2.0.4", "npm-run-all": "^4.1.5", - "prettier": "^2.0.5", - "rimraf": "^3.0.2", - "ts-node": "3.3.0" + "rimraf": "^4.1.2", + "ts-node": "10.9.1" }, "packageManager": "yarn@1.22.19" } diff --git a/src/app.ts b/src/app.ts index b41a651..b2769ab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,15 +2,15 @@ import 'reflect-metadata'; import axios from 'axios'; -import { initDb } from './backend/services/db'; -import { ua } from './backend/services/misskey'; +import { initDb } from './backend/services/db.js'; +import { ua } from './backend/services/misskey.js'; axios.defaults.headers['User-Agent'] = ua; axios.defaults.headers['Content-Type'] = 'application/json'; axios.defaults.validateStatus = (stat) => stat < 500; (async () => { - await initDb(); - (await import('./backend/services/worker')).default(); - (await import('./backend/server')).default(); + await initDb(); + (await import('./backend/services/worker.js')).default(); + (await import('./backend/server.js')).default(); })(); diff --git a/src/backend/const.ts b/src/backend/const.ts index 0d6af5a..61e6276 100644 --- a/src/backend/const.ts +++ b/src/backend/const.ts @@ -11,40 +11,40 @@ export const defaultTemplate = '昨日のMisskeyの活動は\n\nノート: {note 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', - ], + 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; diff --git a/src/backend/controllers/admin.ts b/src/backend/controllers/admin.ts index 2299bc4..7bd804e 100644 --- a/src/backend/controllers/admin.ts +++ b/src/backend/controllers/admin.ts @@ -4,38 +4,38 @@ */ import { BadRequestError, CurrentUser, Get, JsonController, OnUndefined, Post } from 'routing-controllers'; -import { IUser } from '../../common/types/user'; -import { config } from '../../config'; -import { work } from '../services/worker'; -import * as Store from '../store'; +import { IUser } from '../../common/types/user.js'; +import { config } from '../../config.js'; +import { work } from '../services/worker.js'; +import * as Store from '../store.js'; - @JsonController('/admin') +@JsonController('/admin') export class AdminController { - @Get() getAdmin() { - const { username, host } = config.admin; - return { - username, host, - acct: `@${username}@${host}`, - }; - } + @Get() getAdmin() { + const { username, host } = config.admin; + return { + username, host, + acct: `@${username}@${host}`, + }; + } - @Get('/misshai/log') getMisshaiLog(@CurrentUser({ required: true }) user: IUser) { - if (!user.isAdmin) { - throw new BadRequestError('Not an Admin'); - } + @Get('/misshai/log') getMisshaiLog(@CurrentUser({ required: true }) user: IUser) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } - return Store.getState().misshaiWorkerLog; - } + return Store.getState().misshaiWorkerLog; + } - @OnUndefined(204) - @Post('/misshai/start') startMisshai(@CurrentUser({ required: true }) user: IUser) { - if (!user.isAdmin) { - throw new BadRequestError('Not an Admin'); - } - if (Store.getState().nowCalculating) { - throw new BadRequestError('Already started'); - } + @OnUndefined(204) + @Post('/misshai/start') startMisshai(@CurrentUser({ required: true }) user: IUser) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + if (Store.getState().nowCalculating) { + throw new BadRequestError('Already started'); + } - work(); - } + work(); + } } diff --git a/src/backend/controllers/announcement.ts b/src/backend/controllers/announcement.ts index 604830c..31a60cf 100644 --- a/src/backend/controllers/announcement.ts +++ b/src/backend/controllers/announcement.ts @@ -4,101 +4,101 @@ */ import { BadRequestError, Body, CurrentUser, Delete, Get, JsonController, NotFoundError, OnUndefined, Param, Post, Put } from 'routing-controllers'; -import { IUser } from '../../common/types/user'; -import { Announcements } from '../models'; -import { AnnounceCreate } from './body/announce-create'; -import { AnnounceUpdate } from './body/announce-update'; -import { IdProp } from './body/id-prop'; +import { IUser } from '../../common/types/user.js'; +import { Announcements } from '../models/index.js'; +import { AnnounceCreate } from './body/announce-create.js'; +import { AnnounceUpdate } from './body/announce-update.js'; +import { IdProp } from './body/id-prop.js'; - @JsonController('/announcements') -export class AdminController { - @Get() get() { - const query = Announcements.createQueryBuilder('announcement') - .orderBy('"announcement"."createdAt"', 'DESC'); +@JsonController('/announcements') +export class AnnouncementController { + @Get() get() { + const query = Announcements.createQueryBuilder('announcement') + .orderBy('"announcement"."createdAt"', 'DESC'); - return query.getMany(); - } + return query.getMany(); + } - @OnUndefined(204) - @Post() async post(@CurrentUser({ required: true }) user: IUser, @Body({required: true}) {title, body}: AnnounceCreate) { - if (!user.isAdmin) { - throw new BadRequestError('Not an Admin'); - } - if (!title || !body) { - throw new BadRequestError(); - } - await Announcements.insert({ - createdAt: new Date(), - title, - body, - }); - } + @OnUndefined(204) + @Post() async post(@CurrentUser({ required: true }) user: IUser, @Body({required: true}) {title, body}: AnnounceCreate) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + if (!title || !body) { + throw new BadRequestError(); + } + await Announcements.insert({ + createdAt: new Date(), + title, + body, + }); + } - @OnUndefined(204) - @Put() async update(@CurrentUser({ required: true }) user: IUser, @Body() {id, title, body}: AnnounceUpdate) { - if (!user.isAdmin) { - throw new BadRequestError('Not an Admin'); - } - if (!id || !title || !body) { - throw new BadRequestError(); - } - if (!(await Announcements.findOne(id))) { - throw new NotFoundError(); - } + @OnUndefined(204) + @Put() async update(@CurrentUser({ required: true }) user: IUser, @Body() {id, title, body}: AnnounceUpdate) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + if (!id || !title || !body) { + throw new BadRequestError(); + } + if (!(await Announcements.findOne(id))) { + throw new NotFoundError(); + } - await Announcements.update(id, { - title, - body, - }); - } + await Announcements.update(id, { + title, + body, + }); + } - @OnUndefined(204) - @Post('/like/:id') async like(@CurrentUser({ required: true }) user: IUser, @Param('id') id: string) { - if (!user.isAdmin) { - throw new BadRequestError('Not an Admin'); - } - const idNumber = Number(id); - if (isNaN(idNumber)) { - throw new NotFoundError(); - } - if (!id) { - throw new BadRequestError(); - } + @OnUndefined(204) + @Post('/like/:id') async like(@CurrentUser({ required: true }) user: IUser, @Param('id') id: string) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } + const idNumber = Number(id); + if (isNaN(idNumber)) { + throw new NotFoundError(); + } + if (!id) { + throw new BadRequestError(); + } - const announcement = await Announcements.findOne(Number(idNumber)); + const announcement = await Announcements.findOne(Number(idNumber)); - if (!announcement) { - throw new NotFoundError(); - } + if (!announcement) { + throw new NotFoundError(); + } - await Announcements.update(id, { - like: announcement.like + 1, - }); + await Announcements.update(id, { + like: announcement.like + 1, + }); - return announcement.like + 1; - } + return announcement.like + 1; + } - @Delete() async delete(@CurrentUser({ required: true }) user: IUser, @Body() {id}: IdProp) { - if (!user.isAdmin) { - throw new BadRequestError('Not an Admin'); - } + @Delete() async delete(@CurrentUser({ required: true }) user: IUser, @Body() {id}: IdProp) { + if (!user.isAdmin) { + throw new BadRequestError('Not an Admin'); + } - if (!id) { - throw new BadRequestError(); - } + if (!id) { + throw new BadRequestError(); + } - await Announcements.delete(id); - } + await Announcements.delete(id); + } - @Get('/:id') async getDetail(@Param('id') id: string) { - const idNumber = Number(id); - if (isNaN(idNumber)) { - throw new NotFoundError(); - } - const announcement = await Announcements.findOne(idNumber); - if (!announcement) { - throw new NotFoundError(); - } - return announcement; - } + @Get('/:id') async getDetail(@Param('id') id: string) { + const idNumber = Number(id); + if (isNaN(idNumber)) { + throw new NotFoundError(); + } + const announcement = await Announcements.findOne(idNumber); + if (!announcement) { + throw new NotFoundError(); + } + return announcement; + } } diff --git a/src/backend/controllers/body/announce-create.ts b/src/backend/controllers/body/announce-create.ts index 16c4bfb..e413a59 100644 --- a/src/backend/controllers/body/announce-create.ts +++ b/src/backend/controllers/body/announce-create.ts @@ -1,4 +1,4 @@ export class AnnounceCreate { - title: string; - body: string; + title: string; + body: string; } diff --git a/src/backend/controllers/body/announce-update.ts b/src/backend/controllers/body/announce-update.ts index 7b46f20..9903abc 100644 --- a/src/backend/controllers/body/announce-update.ts +++ b/src/backend/controllers/body/announce-update.ts @@ -1,5 +1,5 @@ export class AnnounceUpdate { - id: number; - title: string; - body: string; + id: number; + title: string; + body: string; } diff --git a/src/backend/controllers/body/id-prop.ts b/src/backend/controllers/body/id-prop.ts index ed090c7..1317c3b 100644 --- a/src/backend/controllers/body/id-prop.ts +++ b/src/backend/controllers/body/id-prop.ts @@ -1,3 +1,3 @@ export class IdProp { - id: number; + id: number; } diff --git a/src/backend/controllers/body/user-setting.ts b/src/backend/controllers/body/user-setting.ts index 759837c..285c8d1 100644 --- a/src/backend/controllers/body/user-setting.ts +++ b/src/backend/controllers/body/user-setting.ts @@ -1,25 +1,25 @@ import { IsIn, IsOptional } from 'class-validator'; -import { AlertMode, alertModes } from '../../../common/types/alert-mode'; -import { visibilities, Visibility } from '../../../common/types/visibility'; +import { AlertMode, alertModes } from '../../../common/types/alert-mode.js'; +import { visibilities, Visibility } from '../../../common/types/visibility.js'; export class UserSetting { - @IsIn(alertModes) - @IsOptional() - alertMode?: AlertMode; + @IsIn(alertModes) + @IsOptional() + alertMode?: AlertMode; - @IsIn(visibilities) - @IsOptional() - visibility?: Visibility; + @IsIn(visibilities) + @IsOptional() + visibility?: Visibility; - @IsOptional() - localOnly?: boolean; + @IsOptional() + localOnly?: boolean; - @IsOptional() - remoteFollowersOnly?: boolean; + @IsOptional() + remoteFollowersOnly?: boolean; - @IsOptional() - template?: string; + @IsOptional() + template?: string; - @IsOptional() - useRanking?: boolean; + @IsOptional() + useRanking?: boolean; } diff --git a/src/backend/controllers/index.ts b/src/backend/controllers/index.ts new file mode 100644 index 0000000..4cdc936 --- /dev/null +++ b/src/backend/controllers/index.ts @@ -0,0 +1,13 @@ +import {MetaController} from './meta.js'; +import {AdminController} from './admin.js'; +import {AnnouncementController} from './announcement.js'; +import {RankingController} from './ranking.js'; +import {SessionController} from './session.js'; + +export default [ + MetaController, + AdminController, + AnnouncementController, + RankingController, + SessionController, +]; diff --git a/src/backend/controllers/meta.ts b/src/backend/controllers/meta.ts index 3ebf810..54c0f4c 100644 --- a/src/backend/controllers/meta.ts +++ b/src/backend/controllers/meta.ts @@ -3,19 +3,17 @@ * @author Xeltica */ -import { readFile } from 'fs'; import { Get, JsonController } from 'routing-controllers'; -import { promisify } from 'util'; -import { Meta } from '../../common/types/meta'; -import { currentTokenVersion } from '../const'; +import { Meta } from '../../common/types/meta.js'; +import { currentTokenVersion } from '../const.js'; +import { meta } from '../../config.js'; @JsonController('/meta') export class MetaController { - @Get() async get(): Promise { - const {version} = JSON.parse(await promisify(readFile)(__dirname + '/../../meta.json', { encoding: 'utf-8'})); - return { - version, - currentTokenVersion, - }; - } + @Get() async get(): Promise { + return { + version: meta.version, + currentTokenVersion, + }; + } } diff --git a/src/backend/controllers/ranking.ts b/src/backend/controllers/ranking.ts index 0fa6ed8..43df048 100644 --- a/src/backend/controllers/ranking.ts +++ b/src/backend/controllers/ranking.ts @@ -4,34 +4,34 @@ */ import { Get, JsonController, QueryParam } from 'routing-controllers'; -import { getRanking } from '../functions/ranking'; -import { getUserCount } from '../functions/users'; -import { getState } from '../store'; +import { getRanking } from '../functions/ranking.js'; +import { getUserCount } from '../functions/users.js'; +import { getState } from '../store.js'; @JsonController('/ranking') export class RankingController { - @Get() - async get(@QueryParam('limit', { required: false }) limit?: string) { - return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined); - } + @Get() + async get(@QueryParam('limit', { required: false }) limit?: string) { + return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined); + } - /** - * DBに問い合わせてランキングを取得する - * @param isCalculating 現在算出中かどうか - * @param limit 何件取得するか - * @returns ランキング - */ - private async getResponse(isCalculating: boolean, limit?: number) { - const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({ - id: u.id, - username: u.username, - host: u.host, - rating: u.rating, - })); - return { - isCalculating, - userCount: await getUserCount(), - ranking, - }; - } + /** + * DBに問い合わせてランキングを取得する + * @param isCalculating 現在算出中かどうか + * @param limit 何件取得するか + * @returns ランキング + */ + private async getResponse(isCalculating: boolean, limit?: number) { + const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({ + id: u.id, + username: u.username, + host: u.host, + rating: u.rating, + })); + return { + isCalculating, + userCount: await getUserCount(), + ranking, + }; + } } diff --git a/src/backend/controllers/session.ts b/src/backend/controllers/session.ts index 92faca1..98bd68b 100644 --- a/src/backend/controllers/session.ts +++ b/src/backend/controllers/session.ts @@ -5,45 +5,45 @@ import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Post, Put } from 'routing-controllers'; import { DeepPartial } from 'typeorm'; -import { getScores } from '../functions/get-scores'; -import { deleteUser, updateUser } from '../functions/users'; -import { User } from '../models/entities/user'; -import { sendAlert } from '../services/send-alert'; -import { UserSetting } from './body/user-setting'; +import { getScores } from '../functions/get-scores.js'; +import { deleteUser, updateUser } from '../functions/users.js'; +import { User } from '../models/entities/user.js'; +import { sendAlert } from '../services/send-alert.js'; +import { UserSetting } from './body/user-setting.js'; @JsonController('/session') export class SessionController { - @Get() get(@CurrentUser({ required: true }) user: User) { - return user; - } + @Get() get(@CurrentUser({ required: true }) user: User) { + return user; + } - @Get('/score') - async getScore(@CurrentUser({ required: true }) user: User) { - return getScores(user); - } + @Get('/score') + async getScore(@CurrentUser({ required: true }) user: User) { + return getScores(user); + } - @OnUndefined(204) - @Put() async updateSetting(@CurrentUser({ required: true }) user: User, @Body() setting: UserSetting) { - const s: DeepPartial = {}; - if (setting.alertMode != null) s.alertMode = setting.alertMode; - if (setting.visibility != null) s.visibility = setting.visibility; - if (setting.localOnly != null) s.localOnly = setting.localOnly; - if (setting.remoteFollowersOnly != null) s.remoteFollowersOnly = setting.remoteFollowersOnly; - if (setting.template !== undefined) s.template = setting.template; - if (setting.useRanking !== undefined) s.useRanking = setting.useRanking; - if (Object.keys(s).length === 0) return; - await updateUser(user.username, user.host, s); - } + @OnUndefined(204) + @Put() async updateSetting(@CurrentUser({ required: true }) user: User, @Body() setting: UserSetting) { + const s: DeepPartial = {}; + if (setting.alertMode != null) s.alertMode = setting.alertMode; + if (setting.visibility != null) s.visibility = setting.visibility; + if (setting.localOnly != null) s.localOnly = setting.localOnly; + if (setting.remoteFollowersOnly != null) s.remoteFollowersOnly = setting.remoteFollowersOnly; + if (setting.template !== undefined) s.template = setting.template; + if (setting.useRanking !== undefined) s.useRanking = setting.useRanking; + if (Object.keys(s).length === 0) return; + await updateUser(user.username, user.host, s); + } - @OnUndefined(204) - @Post('/alert') async testAlert(@CurrentUser({ required: true }) user: User) { - await sendAlert(user); - } + @OnUndefined(204) + @Post('/alert') async testAlert(@CurrentUser({ required: true }) user: User) { + await sendAlert(user); + } - @OnUndefined(204) - @Delete() async delete(@CurrentUser({ required: true }) user: User) { - await deleteUser(user.username, user.host); - } + @OnUndefined(204) + @Delete() async delete(@CurrentUser({ required: true }) user: User) { + await deleteUser(user.username, user.host); + } } diff --git a/src/backend/die.ts b/src/backend/die.ts index b3844ca..300cd4e 100644 --- a/src/backend/die.ts +++ b/src/backend/die.ts @@ -1,7 +1,7 @@ import { Context } from 'koa'; -import { ErrorCode } from '../common/types/error-code'; +import { ErrorCode } from '../common/types/error-code.js'; export const die = (ctx: Context, error: ErrorCode = 'other', status = 400): Promise => { - ctx.status = status; - return ctx.render('frontend', { error }); + ctx.status = status; + return ctx.render('frontend', { error }); }; diff --git a/src/backend/functions/error-to-string.ts b/src/backend/functions/error-to-string.ts index 37e6934..0172c33 100644 --- a/src/backend/functions/error-to-string.ts +++ b/src/backend/functions/error-to-string.ts @@ -1,8 +1,8 @@ -import {MisskeyError} from '../services/misskey'; +import {MisskeyError} from '../services/misskey.js'; export const errorToString = (e: Error) => { - if (e instanceof MisskeyError) { - return JSON.stringify(e.error); - } - return `${e.name}: ${e.message}\n${e.stack}`; + if (e instanceof MisskeyError) { + return JSON.stringify(e.error, null, ' '); + } + return `${e.name}: ${e.message}\n${e.stack}`; }; diff --git a/src/backend/functions/gen-token.ts b/src/backend/functions/gen-token.ts index 25bdd68..53c9c47 100644 --- a/src/backend/functions/gen-token.ts +++ b/src/backend/functions/gen-token.ts @@ -1,16 +1,16 @@ import rndstr from 'rndstr'; -import { UsedToken } from '../models/entities/used-token'; -import { UsedTokens } from '../models'; +import { UsedToken } from '../models/entities/used-token.js'; +import { UsedTokens } from '../models/index.js'; /** * トークンを生成します */ export const genToken = async (): Promise => { - let used: UsedToken | undefined = undefined; - let token: string; - do { - token = rndstr(32); - used = await UsedTokens.findOne({ token }); - } while (used !== undefined); - return token; + let used: UsedToken | undefined = undefined; + let token: string; + do { + token = rndstr(32); + used = await UsedTokens.findOne({ token }); + } while (used !== undefined); + return token; }; diff --git a/src/backend/functions/get-scores.ts b/src/backend/functions/get-scores.ts index 739c66d..67d425a 100644 --- a/src/backend/functions/get-scores.ts +++ b/src/backend/functions/get-scores.ts @@ -1,9 +1,9 @@ -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'; +import { User } from '../models/entities/user.js'; +import { toSignedString } from '../../common/functions/to-signed-string.js'; +import {Count} from '../models/count.js'; +import {api} from '../services/misskey.js'; +import {Score} from '../../common/types/score.js'; +import {MiUser} from './update-score.js'; /** * ユーザーのスコアを取得します。 @@ -11,15 +11,15 @@ import {MiUser} from './update-score'; * @returns ユーザーのスコア */ export const getScores = async (user: User): Promise => { - // TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも - const miUser = await api(user.host, 'users/show', { username: user.username }, user.token); + // 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), - }; + return { + notesCount: miUser.notesCount, + followingCount: miUser.followingCount, + followersCount: miUser.followersCount, + ...getDelta(user, miUser), + }; }; /** @@ -29,9 +29,9 @@ export const getScores = async (user: User): Promise => { * @returns ユーザーのスコア差分 */ export const getDelta = (user: User, count: Count) => { - return { - notesDelta: toSignedString(count.notesCount - user.prevNotesCount), - followingDelta: toSignedString(count.followingCount - user.prevFollowingCount), - followersDelta: toSignedString(count.followersCount - user.prevFollowersCount), - }; + return { + notesDelta: toSignedString(count.notesCount - user.prevNotesCount), + followingDelta: toSignedString(count.followingCount - user.prevFollowingCount), + followersDelta: toSignedString(count.followersCount - user.prevFollowersCount), + }; }; diff --git a/src/backend/functions/ranking.ts b/src/backend/functions/ranking.ts index 1c5a699..2f37847 100644 --- a/src/backend/functions/ranking.ts +++ b/src/backend/functions/ranking.ts @@ -1,5 +1,5 @@ -import { Users } from '../models'; -import { User } from '../models/entities/user'; +import { Users } from '../models/index.js'; +import { User } from '../models/entities/user.js'; /** * ミス廃ランキングを取得する @@ -7,15 +7,15 @@ import { User } from '../models/entities/user'; * @returns ミス廃ランキング */ export const getRanking = async (limit?: number | null): Promise => { - const query = Users.createQueryBuilder('user') - .where('"user"."useRanking" IS TRUE') - .andWhere('"user"."bannedFromRanking" IS NOT TRUE') - .andWhere('"user"."rating" <> \'NaN\'') - .orderBy('"user".rating', 'DESC'); + const query = Users.createQueryBuilder('user') + .where('"user"."useRanking" IS TRUE') + .andWhere('"user"."bannedFromRanking" IS NOT TRUE') + .andWhere('"user"."rating" <> \'NaN\'') + .orderBy('"user".rating', 'DESC'); - if (limit) { - query.limit(limit); - } + if (limit) { + query.limit(limit); + } - return await query.getMany(); + return await query.getMany(); }; diff --git a/src/backend/functions/update-rating.ts b/src/backend/functions/update-rating.ts index bd4b17d..fb420b6 100644 --- a/src/backend/functions/update-rating.ts +++ b/src/backend/functions/update-rating.ts @@ -1,8 +1,8 @@ import dayjs from 'dayjs'; -import { User } from '../models/entities/user'; -import { updateUser } from './users'; -import { MiUser } from './update-score'; +import { User } from '../models/entities/user.js'; +import { updateUser } from './users.js'; +import { MiUser } from './update-score.js'; /** * ユーザーのレーティングを更新します @@ -10,9 +10,9 @@ import { MiUser } from './update-score'; * @param miUser Misskeyのユーザー */ export const updateRating = async (user: User, miUser: MiUser): Promise => { - const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1; - await updateUser(user.username, user.host, { - prevRating: user.rating, - rating: miUser.notesCount / elapsedDays, - }); + const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1; + await updateUser(user.username, user.host, { + prevRating: user.rating, + rating: miUser.notesCount / elapsedDays, + }); }; diff --git a/src/backend/functions/update-score.ts b/src/backend/functions/update-score.ts index 2e840c3..1bc4f10 100644 --- a/src/backend/functions/update-score.ts +++ b/src/backend/functions/update-score.ts @@ -1,15 +1,15 @@ -import { User } from '../models/entities/user'; -import { updateUser } from './users'; -import {Count} from '../models/count'; +import { User } from '../models/entities/user.js'; +import { updateUser } from './users.js'; +import {Count} from '../models/count.js'; /** * Misskeyのユーザーモデル */ export type MiUser = { - notesCount: number, - followingCount: number, - followersCount: number, - createdAt: string, + notesCount: number, + followingCount: number, + followersCount: number, + createdAt: string, }; /** @@ -18,9 +18,9 @@ export type MiUser = { * @param count 統計 */ export const updateScore = async (user: User, count: Count): Promise => { - await updateUser(user.username, user.host, { - prevNotesCount: count.notesCount ?? 0, - prevFollowingCount: count.followingCount ?? 0, - prevFollowersCount: count.followersCount ?? 0, - }); + await updateUser(user.username, user.host, { + prevNotesCount: count.notesCount ?? 0, + prevFollowingCount: count.followingCount ?? 0, + prevFollowersCount: count.followersCount ?? 0, + }); }; diff --git a/src/backend/functions/users.ts b/src/backend/functions/users.ts index f88f155..5092f06 100644 --- a/src/backend/functions/users.ts +++ b/src/backend/functions/users.ts @@ -1,22 +1,22 @@ -import { User } from '../models/entities/user'; -import { Users } from '../models'; +import { User } from '../models/entities/user.js'; +import { Users } from '../models/index.js'; import { DeepPartial } from 'typeorm'; -import { genToken } from './gen-token'; -import { IUser } from '../../common/types/user'; -import { config } from '../../config'; -import { currentTokenVersion } from '../const'; +import { genToken } from './gen-token.js'; +import { IUser } from '../../common/types/user.js'; +import { config } from '../../config.js'; +import { currentTokenVersion } from '../const.js'; /** * IUser インターフェイスに変換します。 */ const packUser = (user: User | undefined): IUser | undefined => { - if (!user) return undefined; - const { username: adminName, host: adminHost } = config.admin; + if (!user) return undefined; + const { username: adminName, host: adminHost } = config.admin; - return { - ...user, - isAdmin: adminName === user.username && adminHost === user.host, - }; + return { + ...user, + isAdmin: adminName === user.username && adminHost === user.host, + }; }; /** @@ -26,7 +26,7 @@ const packUser = (user: User | undefined): IUser | undefined => { * @returns ユーザー */ export const getUser = (username: string, host: string): Promise => { - return Users.findOne({ username, host }).then(packUser); + return Users.findOne({ username, host }).then(packUser); }; /** @@ -35,13 +35,13 @@ export const getUser = (username: string, host: string): Promise => { - const id = typeof user === 'number' - ? user - : user.id; + const id = typeof user === 'number' + ? user + : user.id; - const misshaiToken = await genToken(); - Users.update(id, { misshaiToken }); - return misshaiToken; + const misshaiToken = await genToken(); + Users.update(id, { misshaiToken }); + return misshaiToken; }; /** @@ -50,7 +50,7 @@ export const updateUsersToolsToken = async (user: User | User['id']): Promise => { - return Users.findOne({ misshaiToken: token }).then(packUser); + return Users.findOne({ misshaiToken: token }).then(packUser); }; /** @@ -60,13 +60,13 @@ export const getUserByToolsToken = (token: string): Promise = * @param token トークン */ export const upsertUser = async (username: string, host: string, token: string): Promise => { - const u = await getUser(username, host); - if (u) { - await Users.update(u.id, { token, tokenVersion: currentTokenVersion }); - } else { - const result = await Users.save({ username, host, token, tokenVersion: currentTokenVersion }); - await updateUsersToolsToken(result.id); - } + const u = await getUser(username, host); + if (u) { + await Users.update(u.id, { token, tokenVersion: currentTokenVersion }); + } else { + const result = await Users.save({ username, host, token, tokenVersion: currentTokenVersion }); + await updateUsersToolsToken(result.id); + } }; /** @@ -76,7 +76,7 @@ export const upsertUser = async (username: string, host: string, token: string): * @param record 既存のユーザー情報 */ export const updateUser = async (username: string, host: string, record: DeepPartial): Promise => { - await Users.update({ username, host }, record); + await Users.update({ username, host }, record); }; /** @@ -85,7 +85,7 @@ export const updateUser = async (username: string, host: string, record: DeepPar * @param host ホスト名 */ export const deleteUser = async (username: string, host: string): Promise => { - await Users.delete({ username, host }); + await Users.delete({ username, host }); }; /** @@ -93,5 +93,5 @@ export const deleteUser = async (username: string, host: string): Promise * @returns ユーザー数 */ export const getUserCount = (): Promise => { - return Users.count(); + return Users.count(); }; diff --git a/src/backend/models/count.ts b/src/backend/models/count.ts index 65e058e..1baf96c 100644 --- a/src/backend/models/count.ts +++ b/src/backend/models/count.ts @@ -1,6 +1,6 @@ export interface Count { - notesCount: number; - followingCount: number; - followersCount: number; + notesCount: number; + followingCount: number; + followersCount: number; } diff --git a/src/backend/models/entities/announcement.ts b/src/backend/models/entities/announcement.ts index 471f60d..adadcc0 100644 --- a/src/backend/models/entities/announcement.ts +++ b/src/backend/models/entities/announcement.ts @@ -1,31 +1,31 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; -import { IAnnouncement } from '../../../common/types/announcement'; +import {Entity, PrimaryGeneratedColumn, Column} from 'typeorm'; +import {IAnnouncement} from '../../../common/types/announcement.js'; @Entity() export class Announcement implements IAnnouncement { - @PrimaryGeneratedColumn() - public id: number; + @PrimaryGeneratedColumn() + public id: number; - @Column({ - type: 'timestamp without time zone', - }) - public createdAt: Date; + @Column({ + type: 'timestamp without time zone', + }) + public createdAt: Date; - @Column({ - type: 'varchar', - length: 128, - }) - public title: string; + @Column({ + type: 'varchar', + length: 128, + }) + public title: string; - @Column({ - type: 'varchar', - length: 8192, - }) - public body: string; + @Column({ + type: 'varchar', + length: 8192, + }) + public body: string; - @Column({ - type: 'integer', - default: 0, - }) - public like: number; + @Column({ + type: 'integer', + default: 0, + }) + public like: number; } diff --git a/src/backend/models/entities/used-token.ts b/src/backend/models/entities/used-token.ts index 17ede3c..0004bc6 100644 --- a/src/backend/models/entities/used-token.ts +++ b/src/backend/models/entities/used-token.ts @@ -3,8 +3,8 @@ import { Entity, Index, PrimaryColumn } from 'typeorm'; @Entity() @Index([ 'token' ], { unique: true }) export class UsedToken { - @PrimaryColumn({ - type: 'varchar' - }) - public token: string; -} \ No newline at end of file + @PrimaryColumn({ + type: 'varchar' + }) + public token: string; +} diff --git a/src/backend/models/entities/user.ts b/src/backend/models/entities/user.ts index ad08e85..f4eadc3 100644 --- a/src/backend/models/entities/user.ts +++ b/src/backend/models/entities/user.ts @@ -1,114 +1,114 @@ import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm'; -import { AlertMode, alertModes } from '../../../common/types/alert-mode'; -import { visibilities, Visibility } from '../../../common/types/visibility'; -import { IUser } from '../../../common/types/user'; +import { AlertMode, alertModes } from '../../../common/types/alert-mode.js'; +import { visibilities, Visibility } from '../../../common/types/visibility.js'; +import { IUser } from '../../../common/types/user.js'; @Entity() @Index(['username', 'host'], { unique: true }) export class User implements IUser { - @PrimaryGeneratedColumn() - public id: number; + @PrimaryGeneratedColumn() + public id: number; - @Column({ - type: 'varchar' - }) - public username: string; + @Column({ + type: 'varchar' + }) + public username: string; - @Column({ - type: 'varchar' - }) - public host: string; + @Column({ + type: 'varchar' + }) + public host: string; - @Column({ - type: 'varchar' - }) - public token: string; + @Column({ + type: 'varchar' + }) + public token: string; - @Column({ - type: 'varchar', - default: '' - }) - public misshaiToken: string; + @Column({ + type: 'varchar', + default: '' + }) + public misshaiToken: string; - @Column({ - type: 'integer', - default: 0, - }) - public prevNotesCount: number; + @Column({ + type: 'integer', + default: 0, + }) + public prevNotesCount: number; - @Column({ - type: 'integer', - default: 0, - }) - public prevFollowingCount: number; + @Column({ + type: 'integer', + default: 0, + }) + public prevFollowingCount: number; - @Column({ - type: 'integer', - default: 0, - }) - public prevFollowersCount: number; + @Column({ + type: 'integer', + default: 0, + }) + public prevFollowersCount: number; - @Column({ - type: 'enum', - enum: alertModes, - default: 'notification' - }) - public alertMode: AlertMode; + @Column({ + type: 'enum', + enum: alertModes, + default: 'notification' + }) + public alertMode: AlertMode; - @Column({ - type: 'enum', - enum: visibilities, - default: 'home', - }) - public visibility: Visibility; + @Column({ + type: 'enum', + enum: visibilities, + default: 'home', + }) + public visibility: Visibility; - @Column({ - type: 'boolean', - default: false, - }) - public localOnly: boolean; + @Column({ + type: 'boolean', + default: false, + }) + public localOnly: boolean; - @Column({ - type: 'boolean', - default: false, - }) - public remoteFollowersOnly: boolean; + @Column({ + type: 'boolean', + default: false, + }) + public remoteFollowersOnly: boolean; - @Column({ - type: 'varchar', - length: 1024, - nullable: true, - }) - public template: string | null; + @Column({ + type: 'varchar', + length: 1024, + nullable: true, + }) + public template: string | null; - @Column({ - type: 'real', - default: 0, - }) - public prevRating: number; + @Column({ + type: 'real', + default: 0, + }) + public prevRating: number; - @Column({ - type: 'real', - default: 0, - }) - public rating: number; + @Column({ + type: 'real', + default: 0, + }) + public rating: number; - @Column({ - type: 'boolean', - default: false, - }) - public useRanking: boolean; + @Column({ + type: 'boolean', + default: false, + }) + public useRanking: boolean; - @Column({ - type: 'boolean', - default: false, - }) - public bannedFromRanking: boolean; + @Column({ + type: 'boolean', + default: false, + }) + public bannedFromRanking: boolean; - @Column({ - type: 'integer', - default: 1, - comment: 'Misskey API トークンのバージョン。現行と違う場合はアップデートを要求する', - }) - public tokenVersion: number; + @Column({ + type: 'integer', + default: 1, + comment: 'Misskey API トークンのバージョン。現行と違う場合はアップデートを要求する', + }) + public tokenVersion: number; } diff --git a/src/backend/models/index.ts b/src/backend/models/index.ts index c9bd934..5008257 100644 --- a/src/backend/models/index.ts +++ b/src/backend/models/index.ts @@ -1,7 +1,7 @@ -import { User } from './entities/user'; -import { UsedToken } from './entities/used-token'; +import { User } from './entities/user.js'; +import { UsedToken } from './entities/used-token.js'; import { getRepository } from 'typeorm'; -import { Announcement } from './entities/announcement'; +import { Announcement } from './entities/announcement.js'; export const Users = getRepository(User); export const UsedTokens = getRepository(UsedToken); diff --git a/src/backend/render.ts b/src/backend/render.ts index eed5750..6a885be 100644 --- a/src/backend/render.ts +++ b/src/backend/render.ts @@ -1,7 +1,12 @@ import views from 'koa-views'; -import { version } from '../meta.json'; +import path from 'path'; +import url from 'url'; + +import { meta } from '../config.js'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); export const render = views(__dirname + '/views', { - extension: 'pug', - options: { version }, + extension: 'pug', + options: { version: meta.version }, }); diff --git a/src/backend/router.ts b/src/backend/router.ts index af70eb9..2f80c00 100644 --- a/src/backend/router.ts +++ b/src/backend/router.ts @@ -8,12 +8,16 @@ import ms from 'ms'; import striptags from 'striptags'; import MarkdownIt from 'markdown-it'; -import { config } from '../config'; -import { upsertUser, getUser, updateUser } from './functions/users'; -import { api } from './services/misskey'; -import { die } from './die'; -import { misskeyAppInfo } from './const'; -import { Announcements } from './models'; +import { config } from '../config.js'; +import { upsertUser, getUser, updateUser } from './functions/users.js'; +import { api } from './services/misskey.js'; +import { die } from './die.js'; +import { misskeyAppInfo } from './const.js'; +import { Announcements } from './models/index.js'; +import path from 'path'; +import url from 'url'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); export const router = new Router(); @@ -23,175 +27,175 @@ const tokenSecretCache: Record = {}; const md = new MarkdownIt(); router.get('/login', async ctx => { - let host = ctx.query.host as string | undefined; - if (!host) { - await die(ctx, 'invalidParamater'); - return; - } + let host = ctx.query.host as string | undefined; + if (!host) { + await die(ctx, 'invalidParamater'); + return; + } - // http://, https://を潰す - host = host.trim().replace(/^https?:\/\//g, '').replace(/\/+/g, ''); + // http://, https://を潰す + host = host.trim().replace(/^https?:\/\//g, '').replace(/\/+/g, ''); - const meta = await api<{ name: string, uri: string, version: string, features: Record }>(host, 'meta', {}).catch(async e => { - if (!(e instanceof Error && e.name === 'Error')) throw e; - await die(ctx, 'hostNotFound'); - }); + const meta = await api<{ name: string, uri: string, version: string, features: Record }>(host, 'meta', {}).catch(async e => { + if (!(e instanceof Error && e.name === 'Error')) throw e; + await die(ctx, 'hostNotFound'); + }); - // NOTE: catchが呼ばれた場合はvoidとなるためundefinedのはず - if (typeof meta === 'undefined') return; + // NOTE: catchが呼ばれた場合はvoidとなるためundefinedのはず + if (typeof meta === 'undefined') return; - if (typeof meta !== 'object') { - await die(ctx, 'other'); - return; - } + if (typeof meta !== 'object') { + await die(ctx, 'other'); + return; + } - if (meta.version.includes('hitori')) { - await die(ctx, 'hitorisskeyIsDenied'); - return; - } + if (meta.version.includes('hitori')) { + await die(ctx, 'hitorisskeyIsDenied'); + return; + } - // NOTE: - // 環境によってはアクセスしたドメインとMisskeyにおけるhostが異なるケースがある - // そういったインスタンスにおいてアカウントの不整合が生じるため、 - // APIから戻ってきたホスト名を正しいものとして、改めて正規化する - host = meta.uri.replace(/^https?:\/\//g, '').replace(/\/+/g, '').trim(); + // NOTE: + // 環境によってはアクセスしたドメインとMisskeyにおけるhostが異なるケースがある + // そういったインスタンスにおいてアカウントの不整合が生じるため、 + // APIから戻ってきたホスト名を正しいものとして、改めて正規化する + host = meta.uri.replace(/^https?:\/\//g, '').replace(/\/+/g, '').trim(); - const { name, permission, description } = misskeyAppInfo; + const { name, permission, description } = misskeyAppInfo; - if (meta.features.miauth) { - // MiAuthを使用する - const callback = encodeURI(`${config.url}/miauth`); + if (meta.features.miauth) { + // MiAuthを使用する + const callback = encodeURI(`${config.url}/miauth`); - const session = uuid(); - const url = `https://${host}/miauth/${session}?name=${encodeURI(name)}&callback=${encodeURI(callback)}&permission=${encodeURI(permission.join(','))}`; - sessionHostCache[session] = host; + const session = uuid(); + const url = `https://${host}/miauth/${session}?name=${encodeURI(name)}&callback=${encodeURI(callback)}&permission=${encodeURI(permission.join(','))}`; + sessionHostCache[session] = host; - ctx.redirect(url); - } else { - // 旧型認証を使用する - const callbackUrl = encodeURI(`${config.url}/legacy-auth`); + ctx.redirect(url); + } else { + // 旧型認証を使用する + const callbackUrl = encodeURI(`${config.url}/legacy-auth`); - const { secret } = await api<{ secret: string }>(host, 'app/create', { - name, description, permission, callbackUrl, - }); + const { secret } = await api<{ secret: string }>(host, 'app/create', { + name, description, permission, callbackUrl, + }); - const { token, url } = await api<{ token: string, url: string }>(host, 'auth/session/generate', { - appSecret: secret - }); + const { token, url } = await api<{ token: string, url: string }>(host, 'auth/session/generate', { + appSecret: secret + }); - sessionHostCache[token] = host; - tokenSecretCache[token] = secret; + sessionHostCache[token] = host; + tokenSecretCache[token] = secret; - ctx.redirect(url); - } + ctx.redirect(url); + } }); router.get('/teapot', async ctx => { - await die(ctx, 'teapot', 418); + await die(ctx, 'teapot', 418); }); router.get('/miauth', async ctx => { - const session = ctx.query.session as string | undefined; - if (!session) { - await die(ctx, 'sessionRequired'); - return; - } - const host = sessionHostCache[session]; - delete sessionHostCache[session]; - if (!host) { - await die(ctx); - console.error('host is null or undefined'); - return; - } + const session = ctx.query.session as string | undefined; + if (!session) { + await die(ctx, 'sessionRequired'); + return; + } + const host = sessionHostCache[session]; + delete sessionHostCache[session]; + if (!host) { + await die(ctx); + console.error('host is null or undefined'); + return; + } - const url = `https://${host}/api/miauth/${session}/check`; - const res = await axios.post(url, {}); - const { token, user } = res.data; + const url = `https://${host}/api/miauth/${session}/check`; + const res = await axios.post(url, {}); + const { token, user } = res.data; - if (!token || !user) { - await die(ctx); - if (!token) console.error('token is null or undefined'); - if (!user) console.error('user is null or undefined'); - return; - } + if (!token || !user) { + await die(ctx); + if (!token) console.error('token is null or undefined'); + if (!user) console.error('user is null or undefined'); + return; + } - await login(ctx, user, host, token); + await login(ctx, user, host, token); }); router.get('/legacy-auth', async ctx => { - const token = ctx.query.token as string | undefined; - if (!token) { - await die(ctx, 'tokenRequired'); - return; - } - const host = sessionHostCache[token]; - delete sessionHostCache[token]; - if (!host) { - await die(ctx); - return; - } - const appSecret = tokenSecretCache[token]; - delete tokenSecretCache[token]; - if (!appSecret) { - await die(ctx); - return; - } + const token = ctx.query.token as string | undefined; + if (!token) { + await die(ctx, 'tokenRequired'); + return; + } + const host = sessionHostCache[token]; + delete sessionHostCache[token]; + if (!host) { + await die(ctx); + return; + } + const appSecret = tokenSecretCache[token]; + delete tokenSecretCache[token]; + if (!appSecret) { + await die(ctx); + return; + } - const { accessToken, user } = await api<{ accessToken: string, user: Record }>(host, 'auth/session/userkey', { - appSecret, token, - }); - const i = crypto.createHash('sha256').update(accessToken + appSecret, 'utf8').digest('hex'); + const { accessToken, user } = await api<{ accessToken: string, user: Record }>(host, 'auth/session/userkey', { + appSecret, token, + }); + const i = crypto.createHash('sha256').update(accessToken + appSecret, 'utf8').digest('hex'); - await login(ctx, user, host, i); + await login(ctx, user, host, i); }); router.get('/assets/(.*)', async ctx => { - await koaSend(ctx as any, ctx.path.replace('/assets/', ''), { - root: `${__dirname}/../assets/`, - maxage: process.env.NODE_ENV !== 'production' ? 0 : ms('7 days'), - }); + await koaSend(ctx as any, ctx.path.replace('/assets/', ''), { + root: `${__dirname}/../assets/`, + maxage: process.env.NODE_ENV !== 'production' ? 0 : ms('7 days'), + }); }); router.get('/api(.*)', async (ctx, next) => { - next(); + next(); }); router.get('/announcements/:id', async (ctx) => { - const a = await Announcements.findOne(ctx.params.id); - const stripped = striptags(md.render(a?.body ?? '').replace(/\n/g, ' ')); - await ctx.render('frontend', a ? { - t: a.title, - d: stripped.length > 80 ? stripped.substring(0, 80) + '…' : stripped, - } : null); + const a = await Announcements.findOne(ctx.params.id); + const stripped = striptags(md.render(a?.body ?? '').replace(/\n/g, ' ')); + await ctx.render('frontend', a ? { + t: a.title, + d: stripped.length > 80 ? stripped.substring(0, 80) + '…' : stripped, + } : null); }); router.get('/__rescue__', async(ctx) => { - await ctx.render('rescue'); + await ctx.render('rescue'); }); router.get('(.*)', async (ctx) => { - await ctx.render('frontend'); + await ctx.render('frontend'); }); async function login(ctx: Context, user: Record, host: string, token: string) { - const isNewcomer = !(await getUser(user.username as string, host)); - await upsertUser(user.username as string, host, token); + const isNewcomer = !(await getUser(user.username as string, host)); + await upsertUser(user.username as string, host, token); - const u = await getUser(user.username as string, host); + const u = await getUser(user.username as string, host); - if (!u) { - await die(ctx); - return; - } + if (!u) { + await die(ctx); + return; + } - if (isNewcomer) { - await updateUser(u.username, u.host, { - prevNotesCount: user.notesCount as number ?? 0, - prevFollowingCount: user.followingCount as number ?? 0, - prevFollowersCount: user.followersCount as number ?? 0, - }); - } + if (isNewcomer) { + await updateUser(u.username, u.host, { + prevNotesCount: user.notesCount as number ?? 0, + prevFollowingCount: user.followingCount as number ?? 0, + prevFollowersCount: user.followersCount as number ?? 0, + }); + } - await ctx.render('frontend', { token: u.misshaiToken }); + await ctx.render('frontend', { token: u.misshaiToken }); } diff --git a/src/backend/server.ts b/src/backend/server.ts index 6ce6b33..5ff5205 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -1,45 +1,44 @@ import Koa from 'koa'; import bodyParser from 'koa-bodyparser'; -import { Action, useKoaServer } from 'routing-controllers'; +import {Action, useKoaServer} from 'routing-controllers'; -import { config } from '../config'; -import { render } from './render'; -import { router } from './router'; -import { getUserByToolsToken } from './functions/users'; -import { version } from '../meta.json'; +import {config, meta} from '../config.js'; +import {render} from './render.js'; +import {router} from './router.js'; +import {getUserByToolsToken} from './functions/users.js'; +import controllers from './controllers/index.js'; import 'reflect-metadata'; export default (): void => { - const app = new Koa(); + const app = new Koa(); - console.log('Misskey Tools v' + version); + console.log('Misskey Tools v' + meta.version); - console.log('Initializing DB connection...'); + console.log('Initializing DB connection...'); - app.use(render); - app.use(bodyParser()); + app.use(render); + app.use(bodyParser()); - useKoaServer(app, { - controllers: [__dirname + '/controllers/**/*{.ts,.js}'], - routePrefix: '/api/v1', - classTransformer: true, - validation: true, - currentUserChecker: async ({ request }: Action) => { - const { authorization } = request.header; - if (!authorization || !authorization.startsWith('Bearer ')) return null; + useKoaServer(app, { + controllers, + routePrefix: '/api/v1', + classTransformer: true, + validation: true, + currentUserChecker: async ({ request }: Action) => { + const { authorization } = request.header; + if (!authorization || !authorization.startsWith('Bearer ')) return null; - const token = authorization.split(' ')[1].trim(); - const user = await getUserByToolsToken(token); - return user; - }, - }); + const token = authorization.split(' ')[1].trim(); + return await getUserByToolsToken(token); + }, + }); - app.use(router.routes()); - app.use(router.allowedMethods()); + app.use(router.routes()); + app.use(router.allowedMethods()); - console.log(`listening port ${config.port}...`); - console.log('App launched!'); + console.log(`listening port ${config.port}...`); + console.log('App launched!'); - app.listen(config.port || 3000); + app.listen(config.port || 3000); }; diff --git a/src/backend/services/db.ts b/src/backend/services/db.ts index 8e2f957..6b9d683 100644 --- a/src/backend/services/db.ts +++ b/src/backend/services/db.ts @@ -1,13 +1,13 @@ import { getConnection, createConnection, Connection } from 'typeorm'; -import { config } from '../../config'; -import { User } from '../models/entities/user'; -import { UsedToken } from '../models/entities/used-token'; -import { Announcement } from '../models/entities/announcement'; +import { config } from '../../config.js'; +import { User } from '../models/entities/user.js'; +import { UsedToken } from '../models/entities/used-token.js'; +import { Announcement } from '../models/entities/announcement.js'; export const entities = [ - User, - UsedToken, - Announcement, + User, + UsedToken, + Announcement, ]; /** @@ -16,26 +16,26 @@ export const entities = [ * @returns 取得または作成したDBコネクション */ export const initDb = async (force = false): Promise => { - // forceがtrueでない限り、既に接続が存在する場合はそれを返す - if (!force) { - try { - const conn = getConnection(); - return Promise.resolve(conn); - } catch (e) { - // noop - console.warn('connection is not found, so create'); - } - } + // forceがtrueでない限り、既に接続が存在する場合はそれを返す + if (!force) { + try { + const conn = getConnection(); + return Promise.resolve(conn); + } catch (e) { + // noop + console.warn('connection is not found, so create'); + } + } - // 接続がないか、forceがtrueの場合は新規作成する - return createConnection({ - type: 'postgres', - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - extra: config.db.extra, - entities, - }); + // 接続がないか、forceがtrueの場合は新規作成する + return createConnection({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + extra: config.db.extra, + entities, + }); }; diff --git a/src/backend/services/misskey.ts b/src/backend/services/misskey.ts index 775bb0b..3c7fb4e 100644 --- a/src/backend/services/misskey.ts +++ b/src/backend/services/misskey.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import {printLog} from '../store'; -import {delay} from '../utils/delay'; +import {printLog} from '../store.js'; +import {delay} from '../utils/delay.js'; export const ua = `Mozilla/5.0 MisskeyTools +https://github.com/shrimpia/misskey-tools Node/${process.version}`; @@ -10,26 +10,26 @@ const RETRY_COUNT = 5; * Misskey APIを呼び出す */ export const api = async = Record>(host: string, endpoint: string, arg: Record, token?: string): Promise => { - const a = { ...arg }; - if (token) { - a.i = token; - } + const a = { ...arg }; + if (token) { + a.i = token; + } - for (let i = 0; i < RETRY_COUNT; i++) { - let data: T; - try { - data = await axios.post(`https://${host}/api/${endpoint}`, a).then(res => res.data); - } catch (e) { - printLog(`接続エラー: ${host}/api/${endpoint} リトライ中 (${i + 1} / ${RETRY_COUNT})\n${e}`, 'error'); - await delay(3000); - continue; - } - if (!('error' in data)) { - return data; - } - throw new MisskeyError((data as any).error); - } - throw new TimedOutError(); + for (let i = 0; i < RETRY_COUNT; i++) { + let data: T; + try { + data = await axios.post(`https://${host}/api/${endpoint}`, a).then(res => res.data); + } catch (e) { + printLog(`接続エラー: ${host}/api/${endpoint} リトライ中 (${i + 1} / ${RETRY_COUNT})\n${e}`, 'error'); + await delay(3000); + continue; + } + if (!(typeof data === 'object' && 'error' in data)) { + return data; + } + throw new MisskeyError((data as any).error); + } + throw new TimedOutError(); }; /** @@ -39,25 +39,25 @@ export const api = async = Record => { - try { - const res = await api(host, 'i', {}, i); - return !res.error; - } catch { - return false; - } + try { + const res = await api(host, 'i', {}, i); + return !res.error; + } catch { + return false; + } }; export class TimedOutError extends Error {} export class MisskeyError extends Error { - constructor(public error: MisskeyErrorObject) { - super(); - } + constructor(public error: MisskeyErrorObject) { + super(); + } } export interface MisskeyErrorObject { - message: string; - code: string; - id: string; - kind: string; + message: string; + code: string; + id: string; + kind: string; } diff --git a/src/backend/services/send-alert.ts b/src/backend/services/send-alert.ts index e1cc472..411517d 100644 --- a/src/backend/services/send-alert.ts +++ b/src/backend/services/send-alert.ts @@ -1,7 +1,7 @@ -import { User } from '../models/entities/user'; -import { api } from './misskey'; -import {format} from '../../common/functions/format'; -import {getScores} from '../functions/get-scores'; +import { User } from '../models/entities/user.js'; +import { api } from './misskey.js'; +import {format} from '../../common/functions/format.js'; +import {getScores} from '../functions/get-scores.js'; /** @@ -9,21 +9,21 @@ 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; - } + 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; + } }; /** @@ -32,16 +32,16 @@ export const sendAlert = async (user: User) => { * @param user ユーザー */ export const sendNoteAlert = async (text: string, user: User) => { - const res = await api>(user.host, 'notes/create', { - text, - visibility: user.visibility, - localOnly: user.localOnly, - remoteFollowersOnly: user.remoteFollowersOnly, - }, user.token); + const res = await api>(user.host, 'notes/create', { + text, + visibility: user.visibility, + localOnly: user.localOnly, + remoteFollowersOnly: user.remoteFollowersOnly, + }, user.token); - if (res.error) { - throw res.error || res; - } + if (res.error) { + throw res.error || res; + } }; /** @@ -50,13 +50,13 @@ export const sendNoteAlert = async (text: string, user: User) => { * @param user ユーザー */ export const sendNotificationAlert = async (text: string, user: User) => { - const res = await api(user.host, 'notifications/create', { - header: 'Misskey Tools', - icon: 'https://i.imgur.com/B991yTl.png', - body: text, - }, user.token); + const res = await api(user.host, 'notifications/create', { + header: 'Misskey Tools', + icon: 'https://i.imgur.com/B991yTl.png', + body: text, + }, user.token); - if (res.error) { - throw res.error || res; - } + if (res.error) { + throw res.error || res; + } }; diff --git a/src/backend/services/worker.ts b/src/backend/services/worker.ts index 853d6b7..fdec77a 100644 --- a/src/backend/services/worker.ts +++ b/src/backend/services/worker.ts @@ -1,18 +1,19 @@ import cron from 'node-cron'; -import { deleteUser } from '../functions/users'; -import { MiUser, updateScore } from '../functions/update-score'; -import { updateRating } from '../functions/update-rating'; -import { Users } from '../models'; -import {sendNoteAlert, sendNotificationAlert} from './send-alert'; -import {api, MisskeyError, TimedOutError} from './misskey'; -import * as Store from '../store'; -import { User } from '../models/entities/user'; -import {groupBy} from '../utils/group-by'; -import {clearLog, printLog} from '../store'; -import {errorToString} from '../functions/error-to-string'; -import {Acct, toAcct} from '../models/acct'; -import {Count} from '../models/count'; -import {format} from '../../common/functions/format'; +import { deleteUser } from '../functions/users.js'; +import { MiUser, updateScore } from '../functions/update-score.js'; +import { updateRating } from '../functions/update-rating.js'; +import { Users } from '../models/index.js'; +import {sendNoteAlert, sendNotificationAlert} from './send-alert.js'; +import {api, MisskeyError, TimedOutError} from './misskey.js'; +import * as Store from '../store.js'; +import { User } from '../models/entities/user.js'; +import {groupBy} from '../utils/group-by.js'; +import {clearLog, printLog} from '../store.js'; +import {errorToString} from '../functions/error-to-string.js'; +import {Acct, toAcct} from '../models/acct.js'; +import {Count} from '../models/count.js'; +import {format} from '../../common/functions/format.js'; +import {delay} from '../utils/delay.js'; const ERROR_CODES_USER_REMOVED = ['NO_SUCH_USER', 'AUTHENTICATION_FAILED', 'YOUR_ACCOUNT_SUSPENDED']; @@ -20,106 +21,109 @@ const ERROR_CODES_USER_REMOVED = ['NO_SUCH_USER', 'AUTHENTICATION_FAILED', 'YOUR const userScoreCache = new Map(); export default (): void => { - cron.schedule('0 0 0 * * *', work); + cron.schedule('0 0 0 * * *', work); }; export const work = async () => { - Store.dispatch({ nowCalculating: true }); + Store.dispatch({ nowCalculating: true }); - clearLog(); - printLog('Started.'); + clearLog(); + printLog('Started.'); - try { - const users = await Users.find(); - const groupedUsers = groupBy(users, u => u.host); + try { + const users = await Users.find(); + const groupedUsers = groupBy(users, u => u.host); - printLog(`${users.length} アカウントのレート計算を開始します。`); - await calculateAllRating(groupedUsers); - Store.dispatch({ nowCalculating: false }); + printLog(`${users.length} アカウントのレート計算を開始します。`); + await calculateAllRating(groupedUsers); + Store.dispatch({ nowCalculating: false }); - printLog(`${users.length} アカウントのアラート送信を開始します。`); - await sendAllAlerts(groupedUsers); + printLog(`${users.length} アカウントのアラート送信を開始します。`); + await sendAllAlerts(groupedUsers); - printLog('ミス廃アラートワーカーは正常に完了しました。'); - } catch (e) { - printLog('ミス廃アラートワーカーが異常終了しました。', 'error'); - printLog(e instanceof Error ? errorToString(e) : e, 'error'); - } finally { - Store.dispatch({ nowCalculating: false }); - } + printLog('ミス廃アラートワーカーは正常に完了しました。'); + } catch (e) { + printLog('ミス廃アラートワーカーが異常終了しました。', 'error'); + printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error'); + } finally { + Store.dispatch({ nowCalculating: false }); + } }; const calculateAllRating = async (groupedUsers: [string, User[]][]) => { - return await Promise.all(groupedUsers.map(kv => calculateRating(...kv))); + return await Promise.all(groupedUsers.map(kv => calculateRating(...kv))); }; const calculateRating = async (host: string, users: User[]) => { - for (const user of users) { - let miUser: MiUser; - try { - miUser = await api(user.host, 'i', {}, user.token); - } catch (e) { - if (!(e instanceof Error)) { - printLog('バグ:エラーオブジェクトはErrorを継承していないといけない', 'error'); - } else if (e instanceof MisskeyError) { - if (ERROR_CODES_USER_REMOVED.includes(e.error.code)) { - // ユーザーが削除されている場合、レコードからも消してとりやめ - printLog(`アカウント ${toAcct(user)} は削除されているか、凍結されているか、トークンを失効しています。そのため、本システムからアカウントを削除します。`, 'warn'); - await deleteUser(user.username, user.host); - } else { - printLog(`Misskey エラー: ${JSON.stringify(e.error)}`, 'error'); - } - } else if (e instanceof TimedOutError) { - printLog(`サーバー ${user.host} との接続に失敗したため、このサーバーのレート計算を中断します。`, 'error'); - return; - } else { - // おそらく通信エラー - printLog(`不明なエラーが発生しました。\n${errorToString(e)}`, 'error'); - } - continue; - } - userScoreCache.set(toAcct(user), miUser); + for (const user of users) { + let miUser: MiUser; + try { + miUser = await api(user.host, 'i', {}, user.token); + } catch (e) { + if (!(e instanceof Error)) { + printLog('バグ:エラーオブジェクトはErrorを継承していないといけない', 'error'); + } else if (e instanceof MisskeyError) { + if (ERROR_CODES_USER_REMOVED.includes(e.error.code)) { + // ユーザーが削除されている場合、レコードからも消してとりやめ + printLog(`アカウント ${toAcct(user)} は削除されているか、凍結されているか、トークンを失効しています。そのため、本システムからアカウントを削除します。`, 'warn'); + await deleteUser(user.username, user.host); + } else { + printLog(`Misskey エラー: ${JSON.stringify(e.error)}`, 'error'); + } + } else if (e instanceof TimedOutError) { + printLog(`サーバー ${user.host} との接続に失敗したため、このサーバーのレート計算を中断します。`, 'error'); + return; + } else { + // おそらく通信エラー + printLog(`不明なエラーが発生しました。\n${errorToString(e)}`, 'error'); + } + continue; + } + userScoreCache.set(toAcct(user), miUser); - await updateRating(user, miUser); - } - printLog(`${host} ユーザー(${users.length}人) のレート計算が完了しました。`); + await updateRating(user, miUser); + } + printLog(`${host} ユーザー(${users.length}人) のレート計算が完了しました。`); }; const sendAllAlerts = async (groupedUsers: [string, User[]][]) => { - return await Promise.all(groupedUsers.map(kv => sendAlerts(...kv))); + return await Promise.all(groupedUsers.map(kv => sendAlerts(...kv))); }; const sendAlerts = async (host: string, users: User[]) => { - const models = users - .map(user => { - const count = userScoreCache.get(toAcct(user)); - if (count == null) return null; - return { - user, - count, - message: format(user, count), - }; - }) - .filter(u => u != null) as {user: User, count: Count, message: string}[]; + const models = users + .map(user => { + const count = userScoreCache.get(toAcct(user)); + if (count == null) return null; + return { + user, + count, + message: format(user, count), + }; + }) + .filter(u => u != null) as {user: User, count: Count, message: string}[]; - // 何もしない - for (const {user, count} of models.filter(m => m.user.alertMode === 'nothing')) { - await updateScore(user, count); - } + // 何もしない + for (const {user, count} of models.filter(m => m.user.alertMode === 'nothing')) { + await updateScore(user, count); + } - // 通知 - for (const {user, count, message} of models.filter(m => m.user.alertMode === 'notification' || m.user.alertMode === 'both')) { - await sendNotificationAlert(message, user); - if (user.alertMode === 'notification') { - await updateScore(user, count); - } - } + // 通知 + for (const {user, count, message} of models.filter(m => m.user.alertMode === 'notification' || m.user.alertMode === 'both')) { + await sendNotificationAlert(message, user); + if (user.alertMode === 'notification') { + await updateScore(user, count); + } + } - // 通知 - for (const {user, count, message} of models.filter(m => m.user.alertMode === 'note')) { - await sendNoteAlert(message, user); - await updateScore(user, count); - } + // アラート + for (const {user, count, message} of models.filter(m => m.user.alertMode === 'note' || m.user.alertMode === 'both')) { + await sendNoteAlert(message, user); + await Promise.all([ + updateScore(user, count), + delay(1000), + ]); + } - printLog(`${host} ユーザー(${users.length}人) へのアラート送信が完了しました。`); + printLog(`${host} ユーザー(${users.length}人) へのアラート送信が完了しました。`); }; diff --git a/src/backend/store.ts b/src/backend/store.ts index e3d9391..7a9441b 100644 --- a/src/backend/store.ts +++ b/src/backend/store.ts @@ -2,14 +2,14 @@ // getStateを介してステートを取得し、dispatchによって更新する // stateを直接編集できないようになっている -import {Log} from '../common/types/log'; +import {Log} from '../common/types/log.js'; /** * 初期値 */ const defaultState: State = { - nowCalculating: false, - misshaiWorkerLog: [], + nowCalculating: false, + misshaiWorkerLog: [], }; let _state: Readonly = defaultState; @@ -18,8 +18,8 @@ let _state: Readonly = defaultState; * ステートの型 */ export type State = { - nowCalculating: boolean, - misshaiWorkerLog: Log[], + nowCalculating: boolean, + misshaiWorkerLog: Log[], }; /** @@ -33,19 +33,20 @@ export const getState = () => Object.freeze({ ..._state }); * @param mutation ステートの一部を更新するためのオブジェクト */ export const dispatch = (mutation: Partial) => { - _state = { - ..._state, - ...mutation, - }; + _state = { + ..._state, + ...mutation, + }; }; export const clearLog = () => { - dispatch({ misshaiWorkerLog: [] }); + dispatch({ misshaiWorkerLog: [] }); }; export const printLog = (log: unknown, level: Log['level'] = 'info') => { - dispatch({ misshaiWorkerLog: [ - ...getState().misshaiWorkerLog, - { text: String(log), level, timestamp: new Date() }, - ] }); + dispatch({ misshaiWorkerLog: [ + ...getState().misshaiWorkerLog, + { text: String(log), level, timestamp: new Date() }, + ] }); + console[level](log); }; diff --git a/src/backend/utils/group-by.ts b/src/backend/utils/group-by.ts index 56c35ed..d1663ac 100644 --- a/src/backend/utils/group-by.ts +++ b/src/backend/utils/group-by.ts @@ -1,13 +1,13 @@ type GetKeyFunction = (cur: V, idx: number, src: readonly V[]) => K; export const groupBy = (array: readonly V[], getKey: GetKeyFunction) => { - return Array.from( - array.reduce((map, cur, idx, src) => { - const key = getKey(cur, idx, src); - const list = map.get(key); - if (list) list.push(cur); - else map.set(key, [cur]); - return map; - }, new Map()) - ); + return Array.from( + array.reduce((map, cur, idx, src) => { + const key = getKey(cur, idx, src); + const list = map.get(key); + if (list) list.push(cur); + else map.set(key, [cur]); + return map; + }, new Map()) + ); }; diff --git a/src/common/functions/create-gacha.ts b/src/common/functions/create-gacha.ts index 15c5973..2fa3040 100644 --- a/src/common/functions/create-gacha.ts +++ b/src/common/functions/create-gacha.ts @@ -1,12 +1,12 @@ const allKatakana = [ - ...('アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨ'.split('')), - 'ウィ', 'ウェ', - 'キャ', 'キュ', 'キョ', - 'クァ', 'クォ', - 'シャ', 'シュ', 'ショ', - 'チャ', 'チュ', 'チョ', - 'ヒャ', 'ヒュ', 'ヒョ', - 'ミャ', 'ミュ', 'ミョ' + ...('アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨ'.split('')), + 'ウィ', 'ウェ', + 'キャ', 'キュ', 'キョ', + 'クァ', 'クォ', + 'シャ', 'シュ', 'ショ', + 'チャ', 'チュ', 'チョ', + 'ヒャ', 'ヒュ', 'ヒョ', + 'ミャ', 'ミュ', 'ミョ' ]; const allInfix = [ '', 'ー', 'ッ' ]; @@ -15,11 +15,11 @@ const getRandomKatakana = () => allKatakana[Math.floor(Math.random() * allKataka const getRandomInfix = () => allInfix[Math.floor(Math.random() * allInfix.length)]; export const createGacha = () => { - return [ - getRandomKatakana(), - getRandomInfix(), - getRandomKatakana(), - getRandomInfix(), - ...(new Array(Math.floor(Math.random() * 2 + 1)).fill('').map(() => getRandomKatakana())) - ].join(''); + return [ + getRandomKatakana(), + getRandomInfix(), + getRandomKatakana(), + getRandomInfix(), + ...(new Array(Math.floor(Math.random() * 2 + 1)).fill('').map(() => getRandomKatakana())) + ].join(''); }; diff --git a/src/common/functions/format.ts b/src/common/functions/format.ts index e3f5654..1932e2d 100644 --- a/src/common/functions/format.ts +++ b/src/common/functions/format.ts @@ -1,10 +1,10 @@ -import { config } from '../../config'; -import { Score } from '../types/score'; -import { defaultTemplate } from '../../backend/const'; -import { IUser } from '../types/user'; -import { createGacha } from './create-gacha'; -import {Count} from '../../backend/models/count'; -import {getDelta} from '../../backend/functions/get-scores'; +import { config } from '../../config.js'; +import { Score } from '../types/score.js'; +import { defaultTemplate } from '../../backend/const.js'; +import { IUser } from '../types/user.js'; +import { createGacha } from './create-gacha.js'; +import {Count} from '../../backend/models/count.js'; +import {getDelta} from '../../backend/functions/get-scores.js'; /** * 埋め込み変数の型 @@ -15,20 +15,20 @@ export type Variable = string | ((score: Score, user: IUser) => string); * 埋め込み可能な変数のリスト */ export const variables: Record = { - notesCount: score => String(score.notesCount), - followingCount: score => String(score.followingCount), - followersCount: score => String(score.followersCount), - notesDelta: score => String(score.notesDelta), - followingDelta: score => String(score.followingDelta), - followersDelta: score => String(score.followersDelta), - url: config.url, - username: (_, user) => String(user.username), - host: (_, user) => String(user.host), - rating: (_, user) => String(user.rating), - gacha: () => createGacha(), + notesCount: score => String(score.notesCount), + followingCount: score => String(score.followingCount), + followersCount: score => String(score.followersCount), + notesDelta: score => String(score.notesDelta), + followingDelta: score => String(score.followingDelta), + followersDelta: score => String(score.followersDelta), + url: config.url, + username: (_, user) => String(user.username), + host: (_, user) => String(user.host), + rating: (_, user) => String(user.rating), + gacha: () => createGacha(), }; -const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g; +const variableRegex = /\{([a-zA-Z0-9_]+?)}/g; /** * スコア情報とユーザー情報からテキストを生成する @@ -37,13 +37,13 @@ const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g; * @returns 生成したテキスト */ export const format = (user: IUser, count: Count): string => { - const score: Score = { - ...count, - ...getDelta(user, count), - }; - const template = user.template || defaultTemplate; - return template.replace(variableRegex, (m, name) => { - const v = variables[name]; - return !v ? m : typeof v === 'function' ? v(score, user) : v; - }) + '\n\n#misshaialert'; + const score: Score = { + ...count, + ...getDelta(user, count), + }; + const template = user.template || defaultTemplate; + return template.replace(variableRegex, (m, name) => { + const v = variables[name]; + return !v ? m : typeof v === 'function' ? v(score, user) : v; + }) + '\n\n#misshaialert'; }; diff --git a/src/common/types/alert-mode.ts b/src/common/types/alert-mode.ts index f09d16c..60b20c8 100644 --- a/src/common/types/alert-mode.ts +++ b/src/common/types/alert-mode.ts @@ -1,8 +1,8 @@ export const alertModes = [ - 'note', - 'notification', - 'both', - 'nothing' + 'note', + 'notification', + 'both', + 'nothing' ] as const; export type AlertMode = typeof alertModes[number]; diff --git a/src/common/types/announcement.ts b/src/common/types/announcement.ts index 87e9e41..114252f 100644 --- a/src/common/types/announcement.ts +++ b/src/common/types/announcement.ts @@ -1,7 +1,7 @@ export interface IAnnouncement { - id: number; - createdAt: Date; - title: string; - body: string; - like: number; + id: number; + createdAt: Date; + title: string; + body: string; + like: number; } diff --git a/src/common/types/design-system-color.ts b/src/common/types/design-system-color.ts index 6f3422e..bd1c281 100644 --- a/src/common/types/design-system-color.ts +++ b/src/common/types/design-system-color.ts @@ -1,18 +1,18 @@ export const designSystemColors = [ - 'red', - 'vermilion', - 'orange', - 'yellow', - 'lime', - 'green', - 'teal', - 'cyan', - 'skyblue', - 'blue', - 'indigo', - 'purple', - 'magenta', - 'pink', + 'red', + 'vermilion', + 'orange', + 'yellow', + 'lime', + 'green', + 'teal', + 'cyan', + 'skyblue', + 'blue', + 'indigo', + 'purple', + 'magenta', + 'pink', ]; export type DesignSystemColor = typeof designSystemColors[number]; diff --git a/src/common/types/error-code.ts b/src/common/types/error-code.ts index db462d4..855e6d8 100644 --- a/src/common/types/error-code.ts +++ b/src/common/types/error-code.ts @@ -1,13 +1,13 @@ export const errorCodes = [ - 'hitorisskeyIsDenied', - 'teapot', - 'sessionRequired', - 'tokenRequired', - 'invalidParamater', - 'notAuthorized', - 'hostNotFound', - 'invalidHostFormat', - 'other', + 'hitorisskeyIsDenied', + 'teapot', + 'sessionRequired', + 'tokenRequired', + 'invalidParamater', + 'notAuthorized', + 'hostNotFound', + 'invalidHostFormat', + 'other', ] as const; export type ErrorCode = typeof errorCodes[number]; diff --git a/src/common/types/log.ts b/src/common/types/log.ts index a119392..ebc2ccd 100644 --- a/src/common/types/log.ts +++ b/src/common/types/log.ts @@ -1,5 +1,5 @@ export type Log = { - text: string; - level: 'error' | 'warn' | 'info'; - timestamp: Date; + text: string; + level: 'error' | 'warn' | 'info'; + timestamp: Date; } diff --git a/src/common/types/meta.ts b/src/common/types/meta.ts index 9f8d4df..84738d7 100644 --- a/src/common/types/meta.ts +++ b/src/common/types/meta.ts @@ -1,4 +1,4 @@ export interface Meta { - version: string; - currentTokenVersion: number; + version: string; + currentTokenVersion: number; } diff --git a/src/common/types/score.ts b/src/common/types/score.ts index 9d0b01a..08304eb 100644 --- a/src/common/types/score.ts +++ b/src/common/types/score.ts @@ -1,9 +1,9 @@ export interface Score { - notesCount: number; - followingCount: number; - followersCount: number; - notesDelta: string; - followingDelta: string; - followersDelta: string; + notesCount: number; + followingCount: number; + followersCount: number; + notesDelta: string; + followingDelta: string; + followersDelta: string; } diff --git a/src/common/types/user.ts b/src/common/types/user.ts index bfe866b..710c2bb 100644 --- a/src/common/types/user.ts +++ b/src/common/types/user.ts @@ -1,25 +1,25 @@ -import { AlertMode } from './alert-mode'; -import { Visibility } from './visibility'; +import { AlertMode } from './alert-mode.js'; +import { Visibility } from './visibility.js'; export interface IUser { - id: number; - username: string; - host: string; - token: string; - misshaiToken: string; - prevNotesCount: number; - prevFollowingCount: number; - prevFollowersCount: number; - alertMode: AlertMode; - visibility: Visibility; - localOnly: boolean; - remoteFollowersOnly: boolean; - template: string | null; - prevRating: number; - rating: number; - bannedFromRanking: boolean; - isAdmin?: boolean; - tokenVersion: number; - useRanking: boolean; + id: number; + username: string; + host: string; + token: string; + misshaiToken: string; + prevNotesCount: number; + prevFollowingCount: number; + prevFollowersCount: number; + alertMode: AlertMode; + visibility: Visibility; + localOnly: boolean; + remoteFollowersOnly: boolean; + template: string | null; + prevRating: number; + rating: number; + bannedFromRanking: boolean; + isAdmin?: boolean; + tokenVersion: number; + useRanking: boolean; } diff --git a/src/common/types/visibility.ts b/src/common/types/visibility.ts index 7164daa..4077c92 100644 --- a/src/common/types/visibility.ts +++ b/src/common/types/visibility.ts @@ -1,8 +1,8 @@ export const visibilities = [ - 'public', // パブリック - 'home', // ホーム - 'followers', // フォロワー - 'users' // ログインユーザー (Groundpolis 限定) + 'public', // パブリック + 'home', // ホーム + 'followers', // フォロワー + 'users' // ログインユーザー (Groundpolis 限定) ] as const; export type Visibility = typeof visibilities[number]; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index ac40b05..defc9c5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,13 @@ +import path from 'path'; +import url from 'url'; import fs from 'fs'; +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + export const config = Object.freeze(JSON.parse(fs.readFileSync(__dirname + '/../config.json', 'utf-8'))); + +export const meta: MetaJson = Object.freeze(JSON.parse(fs.readFileSync(__dirname + '/meta.json', 'utf-8'))); + +export type MetaJson = { + version: string; +}; diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 7c99ef6..75854bb 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -15,77 +15,77 @@ import {$get} from './misc/api'; import {IUser} from '../common/types/user'; const AppInner : React.VFC = () => { - const { data: session } = useGetSessionQuery(undefined); - const $location = useLocation(); + const { data: session } = useGetSessionQuery(undefined); + const $location = useLocation(); - const dispatch = useDispatch(); + const dispatch = useDispatch(); - useTheme(); + useTheme(); - const {t} = useTranslation(); + const {t} = useTranslation(); - const [error, setError] = useState((window as any).__misshaialert?.error); + const [error, setError] = useState((window as any).__misshaialert?.error); - // ページ遷移がまだされていないかどうか - const [isFirstView, setFirstView] = useState(true); + // ページ遷移がまだされていないかどうか + const [isFirstView, setFirstView] = useState(true); - useEffect(() => { - if (isFirstView) { - setFirstView(false); - } else if (!isFirstView && error) { - setError(null); - } - }, [$location]); + useEffect(() => { + if (isFirstView) { + setFirstView(false); + } else if (!isFirstView && error) { + setError(null); + } + }, [$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 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)); - dispatch(setMobile(qMobile.matches)); - qMobile.addEventListener('change', syncMobile); + useEffect(() => { + const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`); + const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches)); + dispatch(setMobile(qMobile.matches)); + qMobile.addEventListener('change', syncMobile); - return () => { - qMobile.removeEventListener('change', syncMobile); - }; - }, []); + return () => { + qMobile.removeEventListener('change', syncMobile); + }; + }, []); - const TheLayout = session || $location.pathname !== '/' ? GeneralLayout : 'div'; + const TheLayout = session || $location.pathname !== '/' ? GeneralLayout : 'div'; - return ( - - {error ? ( -
-

{t('error')}

-

{t('_error.sorry')}

-

- {t('_error.additionalInfo')} - {t(`_error.${error}`)} -

- {t('retry')} -
- ) : } - - -
- ); + return ( + + {error ? ( +
+

{t('error')}

+

{t('_error.sorry')}

+

+ {t('_error.additionalInfo')} + {t(`_error.${error}`)} +

+ {t('retry')} +
+ ) : } + + +
+ ); }; export const App: React.VFC = () => ( - - - - - + + + + + ); diff --git a/src/frontend/GeneralLayout.tsx b/src/frontend/GeneralLayout.tsx index 0dabf09..9193dd4 100644 --- a/src/frontend/GeneralLayout.tsx +++ b/src/frontend/GeneralLayout.tsx @@ -43,48 +43,48 @@ const MobileHeader = styled.header` `; export const GeneralLayout: React.FC = ({children}) => { - const { data: session } = useGetSessionQuery(undefined); - const { data: meta } = useGetMetaQuery(undefined); - const { isMobile, title, isDrawerShown } = useSelector(state => state.screen); - const {t} = useTranslation(); + const { data: session } = useGetSessionQuery(undefined); + const { data: meta } = useGetMetaQuery(undefined); + const { isMobile, title, isDrawerShown } = useSelector(state => state.screen); + const {t} = useTranslation(); - const dispatch = useDispatch(); + const dispatch = useDispatch(); - return ( - - {isMobile && ( - - -

{t(title ?? 'title')}

-
- )} -
- {!isMobile && ( - - - - )} -
- {session && meta && meta.currentTokenVersion !== session.tokenVersion && ( -
- - {t('shouldUpdateToken')} - - {t('update')} - -
- )} - {children} -
-
-
-
dispatch(setDrawerShown(false))}>
-
e.stopPropagation()}> - -
-
-
- ); + return ( + + {isMobile && ( + + +

{t(title ?? 'title')}

+
+ )} +
+ {!isMobile && ( + + + + )} +
+ {session && meta && meta.currentTokenVersion !== session.tokenVersion && ( +
+ + {t('shouldUpdateToken')} + + {t('update')} + +
+ )} + {children} +
+
+
+
dispatch(setDrawerShown(false))}>
+
e.stopPropagation()}> + +
+
+
+ ); }; diff --git a/src/frontend/Header.tsx b/src/frontend/Header.tsx index a122828..28307c6 100644 --- a/src/frontend/Header.tsx +++ b/src/frontend/Header.tsx @@ -10,22 +10,22 @@ export type HeaderProps = { }; export const Header: React.FC = ({title}) => { - const { t } = useTranslation(); - const { data } = useGetSessionQuery(undefined); - const { isMobile } = useSelector(state => state.screen); + const { t } = useTranslation(); + const { data } = useGetSessionQuery(undefined); + const { isMobile } = useSelector(state => state.screen); - return ( -
-

- {{t('title')}} - {title && <> / {title}} -

- {data && ( - - )} -
- ); + return ( +
+

+ {{t('title')}} + {title && <> / {title}} +

+ {data && ( + + )} +
+ ); }; diff --git a/src/frontend/Modal.tsx b/src/frontend/Modal.tsx index 47a7ff5..676f1db 100644 --- a/src/frontend/Modal.tsx +++ b/src/frontend/Modal.tsx @@ -1,13 +1,13 @@ import React, { useCallback } from 'react'; import { useSelector } from './store'; import { - builtinDialogButtonNo, - builtinDialogButtonOk, - builtinDialogButtonYes, - DialogButton, - DialogButtonType, - DialogIcon, - ModalTypeDialog + builtinDialogButtonNo, + builtinDialogButtonOk, + builtinDialogButtonYes, + DialogButton, + DialogButtonType, + DialogIcon, + ModalTypeDialog } from './modal/dialog'; import { Modal } from './modal/modal'; import { useDispatch } from 'react-redux'; @@ -15,93 +15,93 @@ import { hideModal } from './store/slices/screen'; import { ModalTypeMenu } from './modal/menu'; const getButtons = (button: DialogButtonType): DialogButton[] => { - if (typeof button === 'object') return button; - switch (button) { - case 'ok': return [builtinDialogButtonOk]; - case 'yesNo': return [builtinDialogButtonYes, builtinDialogButtonNo]; - } + if (typeof button === 'object') return button; + switch (button) { + case 'ok': return [builtinDialogButtonOk]; + case 'yesNo': return [builtinDialogButtonYes, builtinDialogButtonNo]; + } }; const dialogIconPattern: Record = { - error: 'fas fa-circle-xmark text-danger', - info: 'fas fa-circle-info text-primary', - question: 'fas fa-circle-question text-primary', - warning: 'fas fa-circle-exclamation text-warning', + error: 'fas fa-circle-xmark text-danger', + info: 'fas fa-circle-info text-primary', + question: 'fas fa-circle-question text-primary', + warning: 'fas fa-circle-exclamation text-warning', }; const Dialog: React.VFC<{modal: ModalTypeDialog}> = ({modal}) => { - const buttons = getButtons(modal.buttons ?? 'ok'); - const dispatch = useDispatch(); + const buttons = getButtons(modal.buttons ?? 'ok'); + const dispatch = useDispatch(); - const onClickButton = useCallback((i: number) => { - dispatch(hideModal()); - if (modal.onSelect) { - modal.onSelect(i); - } - }, [dispatch, modal]); + const onClickButton = useCallback((i: number) => { + dispatch(hideModal()); + if (modal.onSelect) { + modal.onSelect(i); + } + }, [dispatch, modal]); - return ( -
-
- {modal.icon &&
} - {modal.title &&

{modal.title}

} -

{modal.message}

-
- { - buttons.map((b, i) => ( - - )) - } -
-
-
- ); + return ( +
+
+ {modal.icon &&
} + {modal.title &&

{modal.title}

} +

{modal.message}

+
+ { + buttons.map((b, i) => ( + + )) + } +
+
+
+ ); }; const Menu: React.VFC<{modal: ModalTypeMenu}> = ({modal}) => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); - return ( -
- { - modal.items.map((item, i) => ( - - )) - } -
- ); + return ( +
+ { + modal.items.map((item, i) => ( + + )) + } +
+ ); }; const ModalInner = (modal: Modal) => { - switch (modal.type) { - case 'dialog': return ; - case 'menu': return ; - } + switch (modal.type) { + case 'dialog': return ; + case 'menu': return ; + } }; export const ModalComponent: React.VFC = () => { - const shown = useSelector(state => state.screen.modalShown); - const modal = useSelector(state => state.screen.modal); - const dispatch = useDispatch(); - if (!shown || !modal) return null; + const shown = useSelector(state => state.screen.modalShown); + const modal = useSelector(state => state.screen.modal); + const dispatch = useDispatch(); + if (!shown || !modal) return null; - return ( -
dispatch(hideModal())}> -
e.stopPropagation()}> - { ModalInner(modal) } -
-
- ); + return ( +
dispatch(hideModal())}> +
e.stopPropagation()}> + { ModalInner(modal) } +
+
+ ); }; diff --git a/src/frontend/Router.tsx b/src/frontend/Router.tsx index ed7692d..5ea770e 100644 --- a/src/frontend/Router.tsx +++ b/src/frontend/Router.tsx @@ -10,16 +10,16 @@ import { MisshaiPage } from './pages/apps/misshai'; import { NekomimiPage } from './pages/apps/avatar-cropper'; export const Router: React.VFC = () => { - return ( - - - - - - - - - - - ); + return ( + + + + + + + + + + + ); }; diff --git a/src/frontend/components/AnnouncementList.tsx b/src/frontend/components/AnnouncementList.tsx index 5b6c93a..bceed7a 100644 --- a/src/frontend/components/AnnouncementList.tsx +++ b/src/frontend/components/AnnouncementList.tsx @@ -5,30 +5,30 @@ import { IAnnouncement } from '../../common/types/announcement'; import { $get } from '../misc/api'; export const AnnouncementList: React.VFC = () => { - const [announcements, setAnnouncements] = useState([]); + const [announcements, setAnnouncements] = useState([]); - const fetchAllAnnouncements = () => { - setAnnouncements([]); - $get('announcements').then(announcements => { - setAnnouncements(announcements ?? []); - }); - }; + const fetchAllAnnouncements = () => { + setAnnouncements([]); + $get('announcements').then(announcements => { + setAnnouncements(announcements ?? []); + }); + }; - useEffect(() => { - fetchAllAnnouncements(); - }, []); + useEffect(() => { + fetchAllAnnouncements(); + }, []); - if (announcements.length === 0) return null; + if (announcements.length === 0) return null; - return ( - <> -
- {announcements.map(a => ( - - {a.title} - - ))} -
- - ); + return ( + <> +
+ {announcements.map(a => ( + + {a.title} + + ))} +
+ + ); }; diff --git a/src/frontend/components/Card.tsx b/src/frontend/components/Card.tsx index 8d5b87b..5e309d9 100644 --- a/src/frontend/components/Card.tsx +++ b/src/frontend/components/Card.tsx @@ -6,11 +6,11 @@ export type CardProps = { }; export const Card: React.FC = ({children, className, bodyClassName}) => { - return ( -
-
- {children} -
-
- ); + return ( +
+
+ {children} +
+
+ ); }; diff --git a/src/frontend/components/CurrentUser.tsx b/src/frontend/components/CurrentUser.tsx index 4d88e93..9b4b27d 100644 --- a/src/frontend/components/CurrentUser.tsx +++ b/src/frontend/components/CurrentUser.tsx @@ -3,8 +3,8 @@ import { useGetSessionQuery } from '../services/session'; import { Skeleton } from './Skeleton'; export const CurrentUser: React.VFC = () => { - const {data} = useGetSessionQuery(undefined); - return data ? ( -

{data.username}@{data.host}

- ) : ; + const {data} = useGetSessionQuery(undefined); + return data ? ( +

{data.username}@{data.host}

+ ) : ; }; diff --git a/src/frontend/components/HashtagTimeline.tsx b/src/frontend/components/HashtagTimeline.tsx index 8596cb7..7f92426 100644 --- a/src/frontend/components/HashtagTimeline.tsx +++ b/src/frontend/components/HashtagTimeline.tsx @@ -6,12 +6,12 @@ export type HashtagTimelineProps = { }; export const HashtagTimeline: React.VFC = ({hashtag}) => { - const {t} = useTranslation(); - return ( - <> -

{t('_timeline.title')}

-

{t('_timeline.description', { hashtag })}

-

WIP

- - ); + const {t} = useTranslation(); + return ( + <> +

{t('_timeline.title')}

+

{t('_timeline.description', { hashtag })}

+

WIP

+ + ); }; diff --git a/src/frontend/components/LogView.tsx b/src/frontend/components/LogView.tsx index 9a41a57..fcc4ff6 100644 --- a/src/frontend/components/LogView.tsx +++ b/src/frontend/components/LogView.tsx @@ -1,50 +1,50 @@ import React, {useMemo, useState} from 'react'; -import {Log} from '../../common/types/log'; +import {Log} from '../../common/types/log.js'; import dayjs from 'dayjs'; const LogItem: React.FC<{log: Log}> = ({log}) => { - const time = dayjs(log.timestamp).format('hh:mm:ss'); + const time = dayjs(log.timestamp).format('hh:mm:ss'); - return ( -
+ 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 [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'); + 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]); + return levels; + }, [isVisibleError, isVisibleWarn, isVisibleInfo]); - const filteredLog = useMemo(() => log.filter(l => filter.includes(l.level)), [log, filter]); + const filteredLog = useMemo(() => log.filter(l => filter.includes(l.level)), [log, filter]); - return ( - <> - - - -
- {filteredLog.map(l => )} -
- - ); + return ( + <> + + + +
+ {filteredLog.map(l => )} +
+ + ); }; diff --git a/src/frontend/components/LoginForm.tsx b/src/frontend/components/LoginForm.tsx index 4838d6d..d22774e 100644 --- a/src/frontend/components/LoginForm.tsx +++ b/src/frontend/components/LoginForm.tsx @@ -8,39 +8,39 @@ const Input = styled.input` `; export const LoginForm: React.VFC = () => { - const [host, setHost] = useState(''); - const {t} = useTranslation(); + const [host, setHost] = useState(''); + const {t} = useTranslation(); - const login = () => { - location.href = `//${location.host}/login?host=${encodeURIComponent(host)}`; - }; + const login = () => { + location.href = `//${location.host}/login?host=${encodeURIComponent(host)}`; + }; - return ( - - ); + return ( + + ); }; diff --git a/src/frontend/components/NavigationMenu.tsx b/src/frontend/components/NavigationMenu.tsx index 45a1d79..5abf662 100644 --- a/src/frontend/components/NavigationMenu.tsx +++ b/src/frontend/components/NavigationMenu.tsx @@ -9,65 +9,65 @@ import { setDrawerShown } from '../store/slices/screen'; const navLinkClassName = (isActive: boolean) => `item ${isActive ? 'active' : ''}`; export const NavigationMenu: React.VFC = () => { - const { data: session } = useGetSessionQuery(undefined); - const { data: meta } = useGetMetaQuery(undefined); - const {t} = useTranslation(); - const dispatch = useDispatch(); + const { data: session } = useGetSessionQuery(undefined); + const { data: meta } = useGetMetaQuery(undefined); + const {t} = useTranslation(); + const dispatch = useDispatch(); - const onClickItem = () => { - dispatch(setDrawerShown(false)); - }; + const onClickItem = () => { + dispatch(setDrawerShown(false)); + }; - return ( - <> -

{t('title')}

-
-
- - - {t(session ? '_sidebar.dashboard' : '_sidebar.return')} - -
- {session && ( -
-

{t('_sidebar.tools')}

- - - {t('_sidebar.missHaiAlert')} - - - - {t('_sidebar.cropper')} - -
- )} - {session && ( -
-

{session.username}@{session.host}

- - - {t('_sidebar.accounts')} - - - - {t('_sidebar.settings')} - - {session.isAdmin && ( - - - {t('_sidebar.admin')} - - )} -
- )} - {meta && ( -
- + return ( + <> +

{t('title')}

+
- - ); + +
+ )} +
+ + ); }; diff --git a/src/frontend/components/Ranking.tsx b/src/frontend/components/Ranking.tsx index f923b5f..bd8d217 100644 --- a/src/frontend/components/Ranking.tsx +++ b/src/frontend/components/Ranking.tsx @@ -20,46 +20,46 @@ export type RankingProps = { }; export const Ranking: React.VFC = ({limit}) => { - const [response, setResponse] = useState(null); - const [isFetching, setIsFetching] = useState(true); - const [isError, setIsError] = useState(false); - const {t} = useTranslation(); + const [response, setResponse] = useState(null); + const [isFetching, setIsFetching] = useState(true); + const [isError, setIsError] = useState(false); + const {t} = useTranslation(); - // APIコール - useEffect(() => { - setIsFetching(true); - $get(`ranking?limit=${limit ?? ''}`) - .then((result) => { - setResponse(result); - setIsFetching(false); - }) - .catch(c => { - console.error(c); - setIsError(true); - }); - }, [limit, setIsFetching, setIsError]); + // APIコール + useEffect(() => { + setIsFetching(true); + $get(`ranking?limit=${limit ?? ''}`) + .then((result) => { + setResponse(result); + setIsFetching(false); + }) + .catch(c => { + console.error(c); + setIsError(true); + }); + }, [limit, setIsFetching, setIsError]); - return ( - isFetching ? ( -

{t('fetching')}

- ) : isError ? ( -
{t('failedToFetch')}
- ) : response ? ( - response.isCalculating ? ( -

{t('isCalculating')}

- ) : ( - - ) - ) : null - ); + return ( + isFetching ? ( +

{t('fetching')}

+ ) : isError ? ( +
{t('failedToFetch')}
+ ) : response ? ( + response.isCalculating ? ( +

{t('isCalculating')}

+ ) : ( + + ) + ) : null + ); }; diff --git a/src/frontend/components/Skeleton.tsx b/src/frontend/components/Skeleton.tsx index ef34f4b..e34ad29 100644 --- a/src/frontend/components/Skeleton.tsx +++ b/src/frontend/components/Skeleton.tsx @@ -6,7 +6,7 @@ export type SkeletonProps = { }; export const Skeleton: React.VFC = (p) => { - return ( -
- ); + return ( +
+ ); }; diff --git a/src/frontend/components/Tab.tsx b/src/frontend/components/Tab.tsx index 948a164..d6e38a7 100644 --- a/src/frontend/components/Tab.tsx +++ b/src/frontend/components/Tab.tsx @@ -14,20 +14,20 @@ export type TabProps = { // タブコンポーネント export const Tab: React.VFC = (props) => { - return ( -
- {props.items.map((item) => { - return ( - - ); - })} -
- ); + return ( +
+ {props.items.map((item) => { + return ( + + ); + })} +
+ ); }; diff --git a/src/frontend/hooks/useAnnouncements.ts b/src/frontend/hooks/useAnnouncements.ts index 22684b5..c5dd789 100644 --- a/src/frontend/hooks/useAnnouncements.ts +++ b/src/frontend/hooks/useAnnouncements.ts @@ -3,18 +3,18 @@ import { IAnnouncement } from '../../common/types/announcement'; import { $get } from '../misc/api'; export const useAnnouncements = () => { - const [announcements, setAnnouncements] = useState([]); + const [announcements, setAnnouncements] = useState([]); - const fetchAllAnnouncements = () => { - setAnnouncements([]); - $get('announcements').then(announcements => { - setAnnouncements(announcements ?? []); - }); - }; + const fetchAllAnnouncements = () => { + setAnnouncements([]); + $get('announcements').then(announcements => { + setAnnouncements(announcements ?? []); + }); + }; - useEffect(() => { - fetchAllAnnouncements(); - }, []); + useEffect(() => { + fetchAllAnnouncements(); + }, []); - return announcements; + return announcements; }; diff --git a/src/frontend/hooks/useTitle.ts b/src/frontend/hooks/useTitle.ts index 8a997bf..5c25dab 100644 --- a/src/frontend/hooks/useTitle.ts +++ b/src/frontend/hooks/useTitle.ts @@ -3,11 +3,11 @@ import { useDispatch } from 'react-redux'; import { setTitle } from '../store/slices/screen'; export const useTitle = (title: string) => { - const dispatch = useDispatch(); - useEffect(() => { - dispatch(setTitle(title)); - return () => { - dispatch(setTitle(null)); - }; - }, [title]); + const dispatch = useDispatch(); + useEffect(() => { + dispatch(setTitle(title)); + return () => { + dispatch(setTitle(null)); + }; + }, [title]); }; diff --git a/src/frontend/init.tsx b/src/frontend/init.tsx index 6041ad0..aa7ce5d 100644 --- a/src/frontend/init.tsx +++ b/src/frontend/init.tsx @@ -18,17 +18,17 @@ dayjs.extend(relativeTime); let lng = localStorage[LOCALSTORAGE_KEY_LANG]; if (!lng || !Object.keys(languageName).includes(lng)) { - lng = localStorage[LOCALSTORAGE_KEY_LANG] = getBrowserLanguage(); + lng = localStorage[LOCALSTORAGE_KEY_LANG] = getBrowserLanguage(); } i18n - .use(initReactI18next) - .init({ - resources, - lng, - interpolation: { - escapeValue: false // Reactは常にXSS対策をしてくれるので、i18next側では対応不要 - } - }); + .use(initReactI18next) + .init({ + resources, + lng, + interpolation: { + escapeValue: false // Reactは常にXSS対策をしてくれるので、i18next側では対応不要 + } + }); ReactDOM.render(, document.getElementById('app')); diff --git a/src/frontend/langs/index.ts b/src/frontend/langs/index.ts index 93ef029..2df35b2 100644 --- a/src/frontend/langs/index.ts +++ b/src/frontend/langs/index.ts @@ -6,30 +6,30 @@ import jaCR from './ja-cr.json'; import deepmerge from 'deepmerge'; const merge = (baseData: Record, newData: Record) => { - return deepmerge(baseData, newData, { - isMergeableObject: obj => typeof obj === 'object' - }); + return deepmerge(baseData, newData, { + isMergeableObject: obj => typeof obj === 'object' + }); }; const _enUS = merge(jaJP, enUS); export const resources = { - 'ja_JP': { translation: jaJP }, - 'en_US': { translation: _enUS }, - 'ko_KR': { translation: merge(_enUS, koKR) }, - 'ja_CR': { translation: merge(jaJP, jaCR) }, + 'ja_JP': { translation: jaJP }, + 'en_US': { translation: _enUS }, + 'ko_KR': { translation: merge(_enUS, koKR) }, + 'ja_CR': { translation: merge(jaJP, jaCR) }, }; export const languageName = { - 'ja_JP': '日本語', - 'en_US': 'English', - 'ko_KR': '한국어', - 'ja_CR': '怪レい日本语', + 'ja_JP': '日本語', + 'en_US': 'English', + 'ko_KR': '한국어', + 'ja_CR': '怪レい日本语', } as const; export type LanguageCode = keyof typeof resources; export const getBrowserLanguage = () => { - const lang = navigator.language.replace('-', '_').toLowerCase(); - return (Object.keys(resources) as LanguageCode[]).find(k => k.toLowerCase().startsWith(lang)) ?? 'en_US'; + const lang = navigator.language.replace('-', '_').toLowerCase(); + return (Object.keys(resources) as LanguageCode[]).find(k => k.toLowerCase().startsWith(lang)) ?? 'en_US'; }; diff --git a/src/frontend/misc/api.ts b/src/frontend/misc/api.ts index 73c0dfb..bb79a06 100644 --- a/src/frontend/misc/api.ts +++ b/src/frontend/misc/api.ts @@ -3,47 +3,47 @@ import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const'; export type ApiOptions = Record; const getHeaders = (token?: string) => { - const _token = token ?? localStorage.getItem(LOCALSTORAGE_KEY_TOKEN); - const headers: HeadersInit = { - 'Content-Type': 'application/json', - }; - if (_token) { - headers['Authorization'] = `Bearer ${_token}`; - } - return headers; + const _token = token ?? localStorage.getItem(LOCALSTORAGE_KEY_TOKEN); + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + if (_token) { + headers['Authorization'] = `Bearer ${_token}`; + } + return headers; }; const getResponse = (r: Response) => r.status === 204 ? null : r.json() as unknown as T; export const $get = (endpoint: string, token?: string): Promise => { - return fetch(API_ENDPOINT + endpoint, { - method: 'GET', - headers: getHeaders(token), - }).then(r => getResponse(r)); + return fetch(API_ENDPOINT + endpoint, { + method: 'GET', + headers: getHeaders(token), + }).then(r => getResponse(r)); }; export const $put = (endpoint: string, opts: ApiOptions = {}, token?: string): Promise => { - return fetch(API_ENDPOINT + endpoint, { - method: 'PUT', - headers: getHeaders(token), - body: JSON.stringify(opts), - }).then(r => getResponse(r)); + return fetch(API_ENDPOINT + endpoint, { + method: 'PUT', + headers: getHeaders(token), + body: JSON.stringify(opts), + }).then(r => getResponse(r)); }; export const $post = (endpoint: string, opts: ApiOptions = {}, token?: string): Promise => { - return fetch(API_ENDPOINT + endpoint, { - method: 'POST', - headers: getHeaders(token), - body: JSON.stringify(opts), - }).then(r => getResponse(r)); + return fetch(API_ENDPOINT + endpoint, { + method: 'POST', + headers: getHeaders(token), + body: JSON.stringify(opts), + }).then(r => getResponse(r)); }; export const $delete = (endpoint: string, opts: ApiOptions = {}, token?: string): Promise => { - return fetch(API_ENDPOINT + endpoint, { - method: 'DELETE', - headers: getHeaders(token), - body: JSON.stringify(opts), - }).then(r => getResponse(r)); + return fetch(API_ENDPOINT + endpoint, { + method: 'DELETE', + headers: getHeaders(token), + body: JSON.stringify(opts), + }).then(r => getResponse(r)); }; diff --git a/src/frontend/misc/theme.ts b/src/frontend/misc/theme.ts index d7fcb75..fcf22a6 100644 --- a/src/frontend/misc/theme.ts +++ b/src/frontend/misc/theme.ts @@ -2,13 +2,13 @@ import { useCallback, useEffect, useState } from 'react'; import { useSelector } from '../store'; export const actualThemes = [ - 'light', - 'dark', + 'light', + 'dark', ] as const; export const themes = [ - ...actualThemes, - 'system', + ...actualThemes, + 'system', ] as const; export type Theme = typeof themes[number]; @@ -16,42 +16,42 @@ export type Theme = typeof themes[number]; export type ActualTheme = typeof actualThemes[number]; export const useTheme = () => { - const {theme, accentColor} = useSelector(state => state.screen); + const {theme, accentColor} = useSelector(state => state.screen); - const [ osTheme, setOsTheme ] = useState('dark'); + const [ osTheme, setOsTheme ] = useState('dark'); - const applyTheme = useCallback(() => { - const actualTheme = theme === 'system' ? osTheme : theme; - if (actualTheme === 'dark') { - document.body.classList.add('dark'); - } else { - document.body.classList.remove('dark'); - } - }, [theme, osTheme]); + const applyTheme = useCallback(() => { + const actualTheme = theme === 'system' ? osTheme : theme; + if (actualTheme === 'dark') { + document.body.classList.add('dark'); + } else { + document.body.classList.remove('dark'); + } + }, [theme, osTheme]); - // テーマ変更に追従する - useEffect(() => { - applyTheme(); - }, [theme, osTheme]); + // テーマ変更に追従する + useEffect(() => { + applyTheme(); + }, [theme, osTheme]); - // システムテーマ変更に追従する - useEffect(() => { - const q = window.matchMedia('(prefers-color-scheme: dark)'); - setOsTheme(q.matches ? 'dark' : 'light'); + // システムテーマ変更に追従する + useEffect(() => { + const q = window.matchMedia('(prefers-color-scheme: dark)'); + setOsTheme(q.matches ? 'dark' : 'light'); - const listener = () => setOsTheme(q.matches ? 'dark' : 'light'); - q.addEventListener('change', listener); - return () => { - q.removeEventListener('change', listener); - }; - }, [osTheme, setOsTheme]); + const listener = () => setOsTheme(q.matches ? 'dark' : 'light'); + q.addEventListener('change', listener); + return () => { + q.removeEventListener('change', listener); + }; + }, [osTheme, setOsTheme]); - // カラー変更に追従する - useEffect(() => { - const {style} = document.body; - style.setProperty('--primary', `var(--${accentColor})`); - for (let i = 1; i <= 10; i++) { - style.setProperty(`--primary-${i}`, `var(--${accentColor}-${i})`); - } - }, [accentColor]); + // カラー変更に追従する + useEffect(() => { + const {style} = document.body; + style.setProperty('--primary', `var(--${accentColor})`); + for (let i = 1; i <= 10; i++) { + style.setProperty(`--primary-${i}`, `var(--${accentColor}-${i})`); + } + }, [accentColor]); }; diff --git a/src/frontend/modal/dialog.ts b/src/frontend/modal/dialog.ts index f4bce11..4fb9963 100644 --- a/src/frontend/modal/dialog.ts +++ b/src/frontend/modal/dialog.ts @@ -1,11 +1,11 @@ export interface ModalTypeDialog { - type: 'dialog'; - title?: string; - message: string; - icon?: DialogIcon; - buttons?: DialogButtonType; - primaryClassName?: string; - onSelect?: (clickedButtonIndex: number) => void; + type: 'dialog'; + title?: string; + message: string; + icon?: DialogIcon; + buttons?: DialogButtonType; + primaryClassName?: string; + onSelect?: (clickedButtonIndex: number) => void; } export type DialogIcon = 'info' | 'warning' | 'error' | 'question'; @@ -15,20 +15,20 @@ export type DialogButtonType = 'ok' | 'yesNo' | DialogButton[]; export type DialogButtonStyle = 'primary' | 'danger'; export interface DialogButton { - text: string; - style?: DialogButtonStyle; + text: string; + style?: DialogButtonStyle; } export const builtinDialogButtonOk: DialogButton = { - text: 'OK', - style: 'primary', + text: 'OK', + style: 'primary', }; export const builtinDialogButtonYes: DialogButton = { - text: 'はい', - style: 'primary', + text: 'はい', + style: 'primary', }; export const builtinDialogButtonNo: DialogButton = { - text: 'いいえ', + text: 'いいえ', }; diff --git a/src/frontend/modal/menu.ts b/src/frontend/modal/menu.ts index e65e87a..e61a03f 100644 --- a/src/frontend/modal/menu.ts +++ b/src/frontend/modal/menu.ts @@ -1,16 +1,16 @@ export interface ModalTypeMenu { - type: 'menu'; - screenX: number; - screenY: number; - items: MenuItem[]; + type: 'menu'; + screenX: number; + screenY: number; + items: MenuItem[]; } export type MenuItemClassName = `fas fa-${string}`; export interface MenuItem { - icon?: MenuItemClassName; - name: string; - onClick: VoidFunction; - disabled?: boolean; - danger?: boolean; + icon?: MenuItemClassName; + name: string; + onClick: VoidFunction; + disabled?: boolean; + danger?: boolean; } diff --git a/src/frontend/modal/modal.ts b/src/frontend/modal/modal.ts index 7cac00b..eb5aec8 100644 --- a/src/frontend/modal/modal.ts +++ b/src/frontend/modal/modal.ts @@ -2,6 +2,6 @@ import { ModalTypeDialog } from './dialog'; import { ModalTypeMenu } from './menu'; export type Modal = - | ModalTypeMenu - | ModalTypeDialog; + | ModalTypeMenu + | ModalTypeDialog; diff --git a/src/frontend/pages/account.tsx b/src/frontend/pages/account.tsx index 47e3d1f..10e4f05 100644 --- a/src/frontend/pages/account.tsx +++ b/src/frontend/pages/account.tsx @@ -10,59 +10,59 @@ import { Skeleton } from '../components/Skeleton'; import { useTitle } from '../hooks/useTitle'; export const AccountsPage: React.VFC = () => { - const {data} = useGetSessionQuery(undefined); - const {t} = useTranslation(); - const dispatch = useDispatch(); + const {data} = useGetSessionQuery(undefined); + const {t} = useTranslation(); + const dispatch = useDispatch(); - useTitle('_sidebar.accounts'); + useTitle('_sidebar.accounts'); - const {accounts, accountTokens} = useSelector(state => state.screen); + const {accounts, accountTokens} = useSelector(state => state.screen); - const switchAccount = (token: string) => { - const newAccounts = accountTokens.filter(a => a !== token); - newAccounts.push(localStorage.getItem(LOCALSTORAGE_KEY_TOKEN) ?? ''); - localStorage.setItem(LOCALSTORAGE_KEY_ACCOUNTS, JSON.stringify(newAccounts)); - localStorage.setItem(LOCALSTORAGE_KEY_TOKEN, token); - location.reload(); - }; + const switchAccount = (token: string) => { + const newAccounts = accountTokens.filter(a => a !== token); + newAccounts.push(localStorage.getItem(LOCALSTORAGE_KEY_TOKEN) ?? ''); + localStorage.setItem(LOCALSTORAGE_KEY_ACCOUNTS, JSON.stringify(newAccounts)); + localStorage.setItem(LOCALSTORAGE_KEY_TOKEN, token); + location.reload(); + }; - return !data ? ( -
- - - -
- ) : ( -
-
-

{t('_accounts.switchAccount')}

+ return !data ? ( +
+ + + +
+ ) : ( +
+
+

{t('_accounts.switchAccount')}

-
- { - accounts.length === accountTokens.length ? ( - accounts.map(account => ( - - - )) - ) : ( -
...
- ) - } -
-
-
-

{t('_accounts.useAnother')}

- -
-
- ); + + + )) + ) : ( +
...
+ ) + } +
+ +
+

{t('_accounts.useAnother')}

+ +
+ + ); }; diff --git a/src/frontend/pages/admin.tsx b/src/frontend/pages/admin.tsx index c20bf9b..8e8504b 100644 --- a/src/frontend/pages/admin.tsx +++ b/src/frontend/pages/admin.tsx @@ -13,200 +13,200 @@ import {LogView} from '../components/LogView'; export const AdminPage: React.VFC = () => { - const { data, error } = useGetSessionQuery(undefined); + const { data, error } = useGetSessionQuery(undefined); - const dispatch = useDispatch(); + const dispatch = useDispatch(); - useTitle('_sidebar.admin'); + useTitle('_sidebar.admin'); - const [announcements, setAnnouncements] = useState([]); - const [selectedAnnouncement, selectAnnouncement] = useState(null); - const [isAnnouncementsLoaded, setAnnouncementsLoaded] = useState(false); - const [isEditMode, setEditMode] = useState(false); - const [isDeleteMode, setDeleteMode] = useState(false); - const [draftTitle, setDraftTitle] = useState(''); - const [draftBody, setDraftBody] = useState(''); + const [announcements, setAnnouncements] = useState([]); + const [selectedAnnouncement, selectAnnouncement] = useState(null); + const [isAnnouncementsLoaded, setAnnouncementsLoaded] = useState(false); + const [isEditMode, setEditMode] = useState(false); + const [isDeleteMode, setDeleteMode] = useState(false); + const [draftTitle, setDraftTitle] = useState(''); + const [draftBody, setDraftBody] = useState(''); - const [misshaiLog, setMisshaiLog] = useState(null); + const [misshaiLog, setMisshaiLog] = useState(null); - const submitAnnouncement = async () => { - if (selectedAnnouncement) { - await $put('announcements', { - id: selectedAnnouncement.id, - title: draftTitle, - body: draftBody, - }); - } else { - await $post('announcements', { - title: draftTitle, - body: draftBody, - }); - } - selectAnnouncement(null); - setDraftTitle(''); - setDraftBody(''); - setEditMode(false); - fetchAll(); - }; + const submitAnnouncement = async () => { + if (selectedAnnouncement) { + await $put('announcements', { + id: selectedAnnouncement.id, + title: draftTitle, + body: draftBody, + }); + } else { + await $post('announcements', { + title: draftTitle, + body: draftBody, + }); + } + selectAnnouncement(null); + setDraftTitle(''); + setDraftBody(''); + setEditMode(false); + fetchAll(); + }; - const deleteAnnouncement = ({id}: IAnnouncement) => { - $delete('announcements', {id}).then(() => { - fetchAll(); - }); - }; + const deleteAnnouncement = ({id}: IAnnouncement) => { + $delete('announcements', {id}).then(() => { + fetchAll(); + }); + }; - const fetchAll = () => { - setAnnouncements([]); - setAnnouncementsLoaded(false); - $get('announcements').then(announcements => { - setAnnouncements(announcements ?? []); - setAnnouncementsLoaded(true); - }); - fetchLog(); - }; + const fetchAll = () => { + setAnnouncements([]); + setAnnouncementsLoaded(false); + $get('announcements').then(announcements => { + setAnnouncements(announcements ?? []); + setAnnouncementsLoaded(true); + }); + fetchLog(); + }; - const fetchLog = () => { - $get('admin/misshai/log').then(setMisshaiLog); - }; + const fetchLog = () => { + $get('admin/misshai/log').then(setMisshaiLog); + }; - const onClickStartMisshaiAlertWorkerButton = () => { - $post('admin/misshai/start').then(() => { - dispatch(showModal({ - type: 'dialog', - message: '開始', - })); - }).catch((e) => { - dispatch(showModal({ - type: 'dialog', - icon: 'error', - message: e.message, - })); - }); - }; + const onClickStartMisshaiAlertWorkerButton = () => { + $post('admin/misshai/start').then(() => { + dispatch(showModal({ + type: 'dialog', + message: '開始', + })); + }).catch((e) => { + dispatch(showModal({ + type: 'dialog', + icon: 'error', + message: e.message, + })); + }); + }; - /** + /** * Session APIのエラーハンドリング * このAPIがエラーを返した = トークンが無効 なのでトークンを削除してログアウトする */ - useEffect(() => { - if (error) { - console.error(error); - localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN); - location.reload(); - } - }, [error]); + useEffect(() => { + if (error) { + console.error(error); + localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN); + location.reload(); + } + }, [error]); - /** + /** * Edit Modeがオンのとき、Delete Modeを無効化する(誤操作防止) */ - useEffect(() => { - if (isEditMode) { - setDeleteMode(false); - } - }, [isEditMode]); + useEffect(() => { + if (isEditMode) { + setDeleteMode(false); + } + }, [isEditMode]); - /** + /** * お知らせ取得 */ - useEffect(() => { - fetchAll(); - }, []); + useEffect(() => { + fetchAll(); + }, []); - useEffect(() => { - if (selectedAnnouncement) { - setDraftTitle(selectedAnnouncement.title); - setDraftBody(selectedAnnouncement.body); - } else { - setDraftTitle(''); - setDraftBody(''); - } - }, [selectedAnnouncement]); + useEffect(() => { + if (selectedAnnouncement) { + setDraftTitle(selectedAnnouncement.title); + setDraftBody(selectedAnnouncement.body); + } else { + setDraftTitle(''); + setDraftBody(''); + } + }, [selectedAnnouncement]); - return !data || !isAnnouncementsLoaded ? ( -
- - - - -
- ) : ( -
- { - !data.isAdmin ? ( -

You are not an administrator and cannot open this page.

- ) : ( - <> -
-
-

Announcements

- {!isEditMode && ( - - )} - { !isEditMode ? ( - <> - {isDeleteMode &&
Click the item to delete.
} -
- {announcements.map(a => ( - - ))} - {!isDeleteMode && ( - + ))} + {!isDeleteMode && ( + - )} -
- - ) : ( -
-
+ + ) : ( +
+ - +