Compare commits
235 Commits
Author | SHA1 | Date | |
---|---|---|---|
ab81aeb80f | |||
|
ab8de445ca | ||
|
5455ab0bcc | ||
|
ba60b42ac7 | ||
|
f49c0dea6a | ||
|
4544e75da6 | ||
|
05f16b5fc2 | ||
|
d03eeeb9f3 | ||
|
6c93505ef2 | ||
|
063781478b | ||
|
720895df07 | ||
|
064375c9c4 | ||
|
4cabd75eec | ||
|
5b700d1209 | ||
|
fa25b79bed | ||
|
f522c5bd0f | ||
|
5b8d0c0e87 | ||
|
01cb352670 | ||
|
8c697ea8ae | ||
|
3b89540995 | ||
|
1d61c7e740 | ||
|
0c9c43921b | ||
|
40ea641623 | ||
|
0f2707de21 | ||
|
676f50086a | ||
|
3d63c31da3 | ||
|
15707f3871 | ||
|
be3ea5e526 | ||
|
6b370f4aa1 | ||
|
96062b9929 | ||
|
ede2d0a2a2 | ||
|
033304370d | ||
|
53d7531cc4 | ||
|
553577c095 | ||
|
0a624208dc | ||
|
40ca4799c1 | ||
|
cc33a9c155 | ||
|
e301b23e09 | ||
|
7f0f2690ba | ||
|
0b1ebe0628 | ||
|
d0733336e7 | ||
|
a2413e3d3d | ||
|
dc1de5f140 | ||
|
4dfbd474ff | ||
|
46e46a971c | ||
|
82dca8fae1 | ||
|
889397eb30 | ||
|
fcdb85c90f | ||
|
0d9466a456 | ||
|
33aabd788f | ||
|
ea6ca5c9ca | ||
|
0dc86565da | ||
|
8715986083 | ||
|
3e54c5d62e | ||
|
55bfaf4091 | ||
|
eb9eb07435 | ||
|
b352f8e8cb | ||
|
ed0d79dd53 | ||
|
683fe9d43f | ||
|
63b69b9640 | ||
|
2388496647 | ||
|
daa5f2d0c7 | ||
|
4455987778 | ||
|
ea3d21c5fb | ||
|
3a1b28c46e | ||
|
1e5dc134eb | ||
|
c2147a45b0 | ||
|
773c1aa7a2 | ||
|
7d57b381f4 | ||
|
e8a562dfb5 | ||
|
aba5ba390b | ||
|
e2b33b7530 | ||
|
1419416c43 | ||
|
698fd59deb | ||
|
39d750226c | ||
|
a62c78d6d6 | ||
|
6c85bcd9c7 | ||
|
c3d199c6f7 | ||
|
aeb8807178 | ||
|
4dd0161cb3 | ||
|
d3931e5289 | ||
|
ea65239836 | ||
|
2c70325ee0 | ||
|
0461fa470e | ||
|
ffdc32ba47 | ||
|
7a361ebaa0 | ||
|
6ccc3d9aee | ||
|
f1b31b559d | ||
|
892139e2df | ||
|
195455ecf0 | ||
|
ad683ee39f | ||
|
65e447b5ea | ||
|
721adafe8f | ||
|
34b4507e14 | ||
|
d9a9072bd0 | ||
|
527ed4de35 | ||
|
f9afbc0200 | ||
|
8b995f2101 | ||
|
ce90fc46e3 | ||
|
d15b7e5b74 | ||
|
33350adeb5 | ||
|
8096d6dd73 | ||
|
c84af004de | ||
|
becefcd515 | ||
|
163f4b1bbb | ||
|
ddd8ede279 | ||
|
86c88a6a56 | ||
|
6fce1457b6 | ||
|
5fab864ff7 | ||
|
c5c4f94e9b | ||
|
7a444abbca | ||
|
8d927b54bd | ||
|
c7bb861ea7 | ||
|
1c2802ea37 | ||
|
a7adb419fd | ||
|
c6e282b4ba | ||
|
4eb4cddcb1 | ||
|
19a6992cfd | ||
|
3f0eaa9508 | ||
|
b56e2bab38 | ||
|
ad6c9c99ca | ||
|
f0d57b5e0e | ||
|
a533f29d03 | ||
|
66d644a46c | ||
|
53502f2aeb | ||
|
9c5dcb1a9f | ||
|
cb05eb0945 | ||
|
e1dafb9443 | ||
|
7997c5716d | ||
|
86759b6729 | ||
|
48d5a11160 | ||
|
1306d98e78 | ||
|
7826bfde25 | ||
|
2c082389e0 | ||
|
b208974a55 | ||
|
c40bf453e2 | ||
|
d9864814f1 | ||
|
d2dd5bf8dd | ||
|
87fe9a1ce9 | ||
|
cfbca022c4 | ||
|
2236e82c58 | ||
|
808122abff | ||
|
0942fa8273 | ||
|
4be7561d27 | ||
|
94d9f87f9d | ||
|
bdf86160d3 | ||
|
86c9bf02b1 | ||
|
7a0e25bcfd | ||
|
a7bf0c2231 | ||
|
3e4e07c3b3 | ||
|
19ad5eff11 | ||
|
082e5a2ac2 | ||
|
1b9b92fb53 | ||
|
8b25f7623b | ||
|
974236223a | ||
|
82fee5a804 | ||
|
f545567ef0 | ||
|
1e22ef205b | ||
|
084d7f88b9 | ||
|
16eb74ef22 | ||
|
5f44ff2115 | ||
|
dcec09832d | ||
|
ada62246a5 | ||
|
f6c733d1a7 | ||
|
9a450562d6 | ||
|
0aa2644f92 | ||
|
e5659ef95c | ||
|
da40d8f482 | ||
|
67e0edb430 | ||
|
81ed27e8e8 | ||
|
98772da1c3 | ||
|
2af4488194 | ||
|
144daf7bcb | ||
|
484d2b8937 | ||
|
886941ee63 | ||
|
2994ef1467 | ||
|
beb9eee94e | ||
|
43e4d0f2d0 | ||
|
6deb8b4e59 | ||
|
2747a0b613 | ||
|
820c20a1b6 | ||
|
06876c528d | ||
|
ac205b9a9e | ||
|
c2279f4efc | ||
|
f3f94c604d | ||
|
8bd9501dfe | ||
|
d5d55e86bd | ||
|
432a492745 | ||
|
def7d57c76 | ||
|
515ba17c08 | ||
|
bc54efe79d | ||
|
7030c6af37 | ||
|
99108a27cc | ||
|
78f8cc0302 | ||
|
d4e4850ffb | ||
|
8416ae687e | ||
|
3233dd63b0 | ||
|
920cc4354b | ||
|
96a958b867 | ||
|
12a9a645c9 | ||
|
d1ddda9b2c | ||
|
cabdba9ead | ||
|
f7be62ec37 | ||
|
bc984bbd57 | ||
|
2975d96967 | ||
|
ec55fee3c6 | ||
|
d73c8bd90d | ||
|
68c154e6b8 | ||
|
2f27935ebd | ||
|
eaaf7616ce | ||
|
aeef88b79e | ||
|
62580f019e | ||
|
e90338b49c | ||
|
d8f8a7a99a | ||
|
e75bea6126 | ||
|
73275298a8 | ||
|
8b8d5ad752 | ||
|
a5d674310f | ||
|
9c91dfa96e | ||
|
55b5b9e4a6 | ||
|
cb7829abb3 | ||
|
9fa2471714 | ||
|
f6eacca318 | ||
|
88858f70dc | ||
|
3f5ea75b85 | ||
|
26c2f6beb6 | ||
|
51879522d8 | ||
|
95d763713e | ||
|
f0c853e556 | ||
|
229c67bb96 | ||
|
ca0f0e55c2 | ||
|
1d212f6a9a | ||
|
f21d0459a9 | ||
|
a104afa852 | ||
|
9883398c56 |
@ -6,4 +6,4 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_size = 2
|
||||
indent_size = 2
|
||||
|
@ -1,31 +0,0 @@
|
||||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es2020': true
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
'parser': '@typescript-eslint/parser',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 11,
|
||||
'sourceType': 'module'
|
||||
},
|
||||
'plugins': [
|
||||
'@typescript-eslint'
|
||||
],
|
||||
'rules': {
|
||||
'indent': ['error', 2, { 'SwitchCase': 1 } ],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
}
|
||||
};
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
node_modules
|
||||
built
|
||||
node_modules/
|
||||
dist/
|
||||
yarn-error.log
|
||||
config.json
|
||||
.yarn
|
||||
.yarn
|
||||
.env
|
||||
.turbo
|
||||
.DS_Store
|
7
.idea/discord.xml
Normal file
7
.idea/discord.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
@ -6,6 +6,7 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<content url="file://$MODULE_DIR$/node_modules/.pnpm/@prisma+client@4.12.0_prisma@4.12.0/node_modules/.prisma" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="Pug/Jade" />
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="Pug/Jade;SCSS" />
|
||||
</project>
|
@ -1,2 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-1.22.19.cjs
|
||||
nodeLinker: node-modules
|
25
CONTRIBUTING.md
Normal file
25
CONTRIBUTING.md
Normal file
@ -0,0 +1,25 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
Misskey Tools へのコントリビューションにご関心いただき、誠にありがとうございます。
|
||||
|
||||
このドキュメントでは、本プロジェクトの開発方針をまとめています。IssueやPull Requestの作成前に、必ずご一読ください。
|
||||
|
||||
なお、開発の進行に応じて、ドキュメントの内容は大きく変わることがあります。
|
||||
|
||||
## データベース スキーマの編集
|
||||
|
||||
* TypeScriptのコーディング規約に一致するように、命名規則を定める
|
||||
* テーブル名は PascalCase
|
||||
* フィールド名は camelCase
|
||||
* データベース上は `@map` 等を用いて snake_case で保存する
|
||||
* `///` (/3つ)で各要素ごとにコメントをつけること
|
||||
* `//` (/2つ)だと整形時に削除されてしまうため注意
|
||||
* schema.prisma を編集したら、次のコマンドを実行してSQLファイルを生成する
|
||||
* `pnpm dlx prisma migrate dev --name 変更名`
|
||||
* 変更名は snake_case
|
||||
|
||||
## tRPCでのデータ返却時はDTOを渡すこと
|
||||
|
||||
意図しないデータの漏洩を防ぐため、フロントエンドへデータを渡す場合は、必ずDBの実データではなく、DTOへの変換を通してください。
|
||||
|
||||
また、実データを返すと、tRPCの型定義にPrismaの型が含まれてしまい、フロントエンド側からTypeScriptのエラー TS2742 が発生します。
|
30
README.md
30
README.md
@ -1,33 +1,47 @@
|
||||
# Misskey Tools (aka みす廃あらーと)
|
||||
# Misskey Tools
|
||||
|
||||
Misskey Toolsは、Misskeyのために設計された、様々な機能を取り揃えたアカウント管理ツールです。
|
||||
**Ultimate Toolkit for All Misskists.**
|
||||
|
||||
以前は「みす廃あらーと」という、Misskeyでのノート、フォロー、フォロワーの数および前日比を毎日0時にノートするサービスとして開発されていましたが、現在様々な機能に対応したオールインワンツールとして開発中です。
|
||||
すべての Misskey ユーザーがきっと好きになるツールを取り揃えたアカウント管理ツール。
|
||||
|
||||
## サポート
|
||||
|
||||
Misskey Toolsは以下のバージョンのMisskeyを正式にサポートします。
|
||||
|
||||
* Misskey v12.119.2
|
||||
* Misskey v13.x
|
||||
* Meisskey v10.x
|
||||
* Calckey v13.x
|
||||
|
||||
## ビルド
|
||||
|
||||
```
|
||||
# 依存関係の解決
|
||||
yarn install
|
||||
pnpm install
|
||||
|
||||
# アプリケーションのビルド
|
||||
yarn build
|
||||
pnpm run build
|
||||
|
||||
# 実行
|
||||
yarn start
|
||||
pnpm run start
|
||||
|
||||
# デバッグ用に起動
|
||||
yarn dev
|
||||
pnpm run dev
|
||||
|
||||
# Lint
|
||||
pnpm run lint
|
||||
|
||||
# Test (まだ書いてない)
|
||||
pnpm run test
|
||||
|
||||
# デザインシステム ドキュメントを起動
|
||||
pnpm run storybook
|
||||
```
|
||||
|
||||
## コントリビューション
|
||||
|
||||
* 不具合報告や機能要望は [Issue](/shrimpia/misskey-tools/issues)
|
||||
* Pull Requestは [Issue](/shrimpia/misskey-tools/issues)
|
||||
|
||||
## LICENSE
|
||||
|
||||
[AGPL 3.0](LICENSE)
|
||||
|
@ -1,8 +0,0 @@
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
|
||||
const { version } = JSON.parse(readFileSync('./package.json', {
|
||||
encoding: 'UTF-8',
|
||||
flag: 'r',
|
||||
}));
|
||||
|
||||
writeFileSync('built/meta.json', JSON.stringify({ version }));
|
@ -2,16 +2,8 @@
|
||||
"port": 4000,
|
||||
"url": "https://misskey.tools",
|
||||
"uaExtra": "",
|
||||
"db": {
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"user": "user",
|
||||
"pass": "pass",
|
||||
"db": "tools",
|
||||
"extra": {}
|
||||
},
|
||||
"admin": {
|
||||
"username": "your_user_name",
|
||||
"host": "your-instance-host"
|
||||
"port": 6379
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
files:
|
||||
- source: /src/frontend/langs/ja-JP.json
|
||||
translation: /src/frontend/langs/%locale%.json
|
||||
update_option: update_as_unapproved
|
||||
- source: /packages/frontend/src/langs/ja-JP.json
|
||||
translation: /packages/frontend/src/langs/%locale%.json
|
||||
update_option: update_as_unapproved
|
||||
|
@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "usage: $0 <migration-name>"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
npx ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:generate -n $1
|
@ -1,16 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class Init1596513280623 implements MigrationInterface {
|
||||
name = 'Init1596513280623';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query('DROP INDEX "IDX_6269eebacdb25de8569298a52a"');
|
||||
await queryRunner.query('DROP TABLE "user"');
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class Init21596514165166 implements MigrationInterface {
|
||||
name = 'Init21596514165166';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class mypage1599570288522 implements MigrationInterface {
|
||||
name = 'mypage1599570288522';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "misshaiToken"');
|
||||
await queryRunner.query('DROP INDEX "IDX_7f2db4c33c33cd6b38e63393fe"');
|
||||
await queryRunner.query('DROP TABLE "used_token"');
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class mode1599577510614 implements MigrationInterface {
|
||||
name = 'mode1599577510614';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "alertMode"');
|
||||
await queryRunner.query('DROP TYPE "user_alertmode_enum"');
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class visibility1609938844427 implements MigrationInterface {
|
||||
name = 'visibility1609938844427';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"');
|
||||
}
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class template1609941393782 implements MigrationInterface {
|
||||
name = 'template1609941393782';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" ADD "template" character varying(280)');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "template"');
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class rating1609948116186 implements MigrationInterface {
|
||||
name = 'rating1609948116186';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"');
|
||||
}
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class Announcement1633841235323 implements MigrationInterface {
|
||||
name = 'Announcement1633841235323';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE TABLE "announcement" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL, "title" character varying(128) NOT NULL, "body" character varying(8192) NOT NULL, "like" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP TABLE "announcement"');
|
||||
}
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class addTokenVersion1643366857976 implements MigrationInterface {
|
||||
name = 'addTokenVersion1643366857976';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" ADD "tokenVersion" integer NOT NULL DEFAULT 1');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "tokenVersion"');
|
||||
}
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class alertModeBoth1644940446672 implements MigrationInterface {
|
||||
name = 'alertModeBoth1644940446672';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TYPE "public"."user_alertmode_enum" RENAME TO "user_alertmode_enum_old"');
|
||||
await queryRunner.query('CREATE TYPE "user_alertmode_enum" AS ENUM(\'note\', \'notification\', \'both\', \'nothing\')');
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" DROP DEFAULT');
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" TYPE "user_alertmode_enum" USING "alertMode"::"text"::"user_alertmode_enum"');
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" SET DEFAULT \'notification\'');
|
||||
await queryRunner.query('DROP TYPE "user_alertmode_enum_old"');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE TYPE "user_alertmode_enum_old" AS ENUM(\'note\', \'notification\', \'nothing\')');
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" DROP DEFAULT');
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" TYPE "user_alertmode_enum_old" USING "alertMode"::"text"::"user_alertmode_enum_old"');
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "alertMode" SET DEFAULT \'notification\'');
|
||||
await queryRunner.query('DROP TYPE "user_alertmode_enum"');
|
||||
await queryRunner.query('ALTER TYPE "user_alertmode_enum_old" RENAME TO "user_alertmode_enum"');
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class useRanking1651804009671 implements MigrationInterface {
|
||||
name = 'useRanking1651804009671';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" ADD "useRanking" boolean NOT NULL DEFAULT false');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "useRanking"');
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class expandTemplateLength1663226831484 implements MigrationInterface {
|
||||
name = 'expandTemplateLength1663226831484';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "template" TYPE character varying(1024)');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "user" ALTER COLUMN "template" TYPE character varying(280)');
|
||||
}
|
||||
}
|
10
nodemon.json
10
nodemon.json
@ -1,10 +0,0 @@
|
||||
{
|
||||
"watch": [
|
||||
"src"
|
||||
],
|
||||
"ignore": [
|
||||
"src/frontend/*"
|
||||
],
|
||||
"ext": "ts,tsx,pug,scss",
|
||||
"exec": "run-s build:backend start"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const entities = require('./built/backend/services/db').entities;
|
||||
|
||||
const config = Object.freeze(JSON.parse(fs.readFileSync(__dirname + '/config.json', 'utf-8')));
|
||||
|
||||
module.exports = {
|
||||
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: entities,
|
||||
migrations: ['migration/*.ts'],
|
||||
cli: {
|
||||
migrationsDir: 'migration'
|
||||
}
|
||||
};
|
127
package.json
127
package.json
@ -1,112 +1,19 @@
|
||||
{
|
||||
"name": "misskey-tools",
|
||||
"version": "3.2.0",
|
||||
"description": "",
|
||||
"main": "built/app.js",
|
||||
"author": "Shrimpia Network",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "run-s build:backend build:frontend",
|
||||
"build:frontend": "webpack",
|
||||
"build:backend": "run-s build:backend-source build:views build:meta build:styles build:assets",
|
||||
"build:backend-source": "tsc",
|
||||
"build:views": "copyfiles -u 1 src/backend/views/*.pug ./built/",
|
||||
"build:assets": "copyfiles -u 1 assets/* ./built/assets/",
|
||||
"build:meta": "node ./build-meta.js",
|
||||
"build:styles": "sass styles/:built/assets",
|
||||
"start": "node built/app.js",
|
||||
"dev": "run-p 'dev:*'",
|
||||
"dev:backend": "nodemon",
|
||||
"dev:frontend": "webpack --watch",
|
||||
"clean": "rimraf built",
|
||||
"tsc": "tsc",
|
||||
"lint": "eslint --ext .ts,.tsx src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"migrate": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:run",
|
||||
"migrate:revert": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:revert"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@koa/multer": "^3.0.2",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"axios": "^0.21.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"css-loader": "^6.2.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"deepmerge": "^4.2.2",
|
||||
"i18next": "^20.6.1",
|
||||
"i18next-browser-languagedetector": "^6.1.2",
|
||||
"insert-text-at-cursor": "^0.3.0",
|
||||
"json5-loader": "^4.0.1",
|
||||
"koa": "^2.13.0",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"koa-router": "^9.1.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-session": "^6.0.0",
|
||||
"koa-views": "^6.3.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"misskey-js": "^0.0.6",
|
||||
"ms": "^2.1.3",
|
||||
"node-cron": "^2.0.3",
|
||||
"object.pick": "^1.3.0",
|
||||
"pg": "^8.3.0",
|
||||
"pug": "^3.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.12.0",
|
||||
"react-image-crop": "^9.0.5",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-modal-hook": "^3.0.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"react-twemoji": "^0.5.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rndstr": "^1.0.0",
|
||||
"routing-controllers": "^0.10.1",
|
||||
"sass": "^1.38.2",
|
||||
"sass-loader": "^12.1.0",
|
||||
"striptags": "^3.2.0",
|
||||
"style-loader": "^3.2.1",
|
||||
"styled-components": "^5.3.1",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tsc-alias": "^1.3.9",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typeorm": "0.2.25",
|
||||
"typescript": "^4.9.5",
|
||||
"uuid": "^8.3.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"xeltica-ui": "xeltica-studio/design-system"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/insert-text-at-cursor": "^0.3.0",
|
||||
"@types/koa": "^2.11.3",
|
||||
"@types/koa-bodyparser": "^4.3.0",
|
||||
"@types/koa-router": "^7.4.1",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/koa-session": "^5.10.2",
|
||||
"@types/koa-views": "^2.0.4",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^18.14.1",
|
||||
"@types/node-cron": "^2.0.3",
|
||||
"@types/object.pick": "^1.3.1",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-twemoji": "^0.4.0",
|
||||
"@types/styled-components": "^5.1.13",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.53.0",
|
||||
"copyfiles": "^2.3.0",
|
||||
"eslint": "^8.34.0",
|
||||
"nodemon": "^2.0.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rimraf": "^4.1.2",
|
||||
"ts-node": "10.9.1"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
"name": "misskey-tools",
|
||||
"version": "4.0.0-lyc.0",
|
||||
"description": "Fork of Misskey Tools for all Misskists.",
|
||||
"author": "LycheeBridge",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"clean": "turbo run clean",
|
||||
"dev": "turbo run dev --no-cache --continue --force",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"storybook": "pnpm recursive --filter tools-frontend run storybook"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^1.10.2"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.1"
|
||||
}
|
||||
|
3
packages/backend/.eslintrc.cjs
Normal file
3
packages/backend/.eslintrc.cjs
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['tools']
|
||||
}
|
1
packages/backend/example.env
Normal file
1
packages/backend/example.env
Normal file
@ -0,0 +1 @@
|
||||
DATABASE_URL="postgresql://user:pass@localhost:5432/tools?schema=public"
|
74
packages/backend/package.json
Normal file
74
packages/backend/package.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "tools-backend",
|
||||
"version": "4.0.0-dev",
|
||||
"author": "Shrimpia Network",
|
||||
"type": "module",
|
||||
"types": "./dist/app.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && tsup",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsc --noEmit && tsup --watch --onSuccess \"node --enable-source-maps dist/app.js\"",
|
||||
"lint": "eslint --fix \"src/**/*.ts*\"",
|
||||
"start": "node dist/app.js",
|
||||
"start-dev": "node --enable-source-maps dist/app.js",
|
||||
"migrate": "prisma migrate dev",
|
||||
"test": "jest --detectOpenHandles",
|
||||
"migrate:gen": "prisma migrate dev --create-only --name"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/view": "^7.4.1",
|
||||
"@prisma/client": "4.13.0",
|
||||
"@trpc/server": "10.23.0",
|
||||
"axios": "^1.4.0",
|
||||
"bullmq": "^3.14.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dotenv": "^16.0.3",
|
||||
"fastify": "^4.17.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jest": "^29.5.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"misskey-js": "^0.0.15",
|
||||
"node-cron": "^3.0.2",
|
||||
"pg": "^8.10.0",
|
||||
"pino-pretty": "^10.0.0",
|
||||
"pug": "^3.0.2",
|
||||
"rndstr": "^1.0.0",
|
||||
"striptags": "^3.2.0",
|
||||
"tools-jest-presets": "workspace:*",
|
||||
"tools-shared": "workspace:*",
|
||||
"typescript": "5.0.4",
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/node-cron": "^3.0.7",
|
||||
"@types/pug": "^2.0.6",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint-config-tools": "workspace:*",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "4.13.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"tools-tsconfig": "workspace:*",
|
||||
"tsup": "^6.7.0"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/app.ts",
|
||||
"src/scripts/*"
|
||||
],
|
||||
"format": [
|
||||
"esm"
|
||||
],
|
||||
"dts": {
|
||||
"resolve": true
|
||||
},
|
||||
"splitting": false,
|
||||
"clean": true,
|
||||
"sourcemap": true
|
||||
}
|
||||
}
|
54
packages/backend/prisma/migrations/0_init/migration.sql
Normal file
54
packages/backend/prisma/migrations/0_init/migration.sql
Normal file
@ -0,0 +1,54 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "user_alertmode_enum" AS ENUM ('note', 'notification', 'both', 'nothing');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "user_visibility_enum" AS ENUM ('public', 'home', 'followers', 'users');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "announcement" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"createdAt" TIMESTAMP(6) NOT NULL,
|
||||
"title" VARCHAR(128) NOT NULL,
|
||||
"body" VARCHAR(8192) NOT NULL,
|
||||
"like" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "used_token" (
|
||||
"token" VARCHAR NOT NULL,
|
||||
|
||||
CONSTRAINT "PK_7f2db4c33c33cd6b38e63393fe5" PRIMARY KEY ("token")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" VARCHAR NOT NULL,
|
||||
"host" VARCHAR NOT NULL,
|
||||
"token" VARCHAR NOT NULL,
|
||||
"prevNotesCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"prevFollowingCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"prevFollowersCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"misshaiToken" VARCHAR NOT NULL DEFAULT '',
|
||||
"alertMode" "user_alertmode_enum" NOT NULL DEFAULT 'notification',
|
||||
"visibility" "user_visibility_enum" NOT NULL DEFAULT 'home',
|
||||
"localOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"remoteFollowersOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"template" VARCHAR(1024),
|
||||
"prevRating" REAL NOT NULL DEFAULT 0,
|
||||
"rating" REAL NOT NULL DEFAULT 0,
|
||||
"bannedFromRanking" BOOLEAN NOT NULL DEFAULT false,
|
||||
"tokenVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"useRanking" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "IDX_7f2db4c33c33cd6b38e63393fe" ON "used_token"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "IDX_6269eebacdb25de8569298a52a" ON "user"("username", "host");
|
||||
|
@ -0,0 +1,61 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "account" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"access_token" VARCHAR NOT NULL,
|
||||
"email" VARCHAR,
|
||||
"password" VARCHAR,
|
||||
|
||||
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "misskey_session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" VARCHAR NOT NULL,
|
||||
"host" VARCHAR NOT NULL,
|
||||
"token" VARCHAR NOT NULL,
|
||||
"tokenVersion" INTEGER NOT NULL,
|
||||
"accountId" VARCHAR NOT NULL,
|
||||
|
||||
CONSTRAINT "misskey_session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "misshai_account" (
|
||||
"misskey_session_id" TEXT NOT NULL,
|
||||
"alert_as_note" BOOLEAN NOT NULL DEFAULT false,
|
||||
"alert_as_notificaton" BOOLEAN NOT NULL DEFAULT true,
|
||||
"note_visibility" TEXT NOT NULL DEFAULT 'home',
|
||||
"note_local_only" BOOLEAN NOT NULL DEFAULT false,
|
||||
"template" VARCHAR(1024),
|
||||
"banned_from_ranking" BOOLEAN NOT NULL DEFAULT false,
|
||||
"ranking_visible" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "misshai_account_pkey" PRIMARY KEY ("misskey_session_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "misshai_record" (
|
||||
"id" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"notes_count" INTEGER NOT NULL,
|
||||
"following_count" INTEGER NOT NULL,
|
||||
"followers_count" INTEGER NOT NULL,
|
||||
"rating" REAL NOT NULL DEFAULT 0,
|
||||
"account_id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "misshai_record_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "account_access_token_key" ON "account"("access_token");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "misskey_session" ADD CONSTRAINT "misskey_session_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "misshai_account" ADD CONSTRAINT "misshai_account_misskey_session_id_fkey" FOREIGN KEY ("misskey_session_id") REFERENCES "misskey_session"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "misshai_record" ADD CONSTRAINT "misshai_record_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "misshai_account"("misskey_session_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,11 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "misshai_record" DROP CONSTRAINT "misshai_record_account_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "misskey_session" DROP CONSTRAINT "misskey_session_accountId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "misskey_session" ADD CONSTRAINT "misskey_session_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "misshai_record" ADD CONSTRAINT "misshai_record_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "misshai_account"("misskey_session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "misshai_account" DROP CONSTRAINT "misshai_account_misskey_session_id_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "misshai_account" ADD CONSTRAINT "misshai_account_misskey_session_id_fkey" FOREIGN KEY ("misskey_session_id") REFERENCES "misskey_session"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[username,host]` on the table `misskey_session` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "misskey_session_username_host_key" ON "misskey_session"("username", "host");
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "account" ADD COLUMN "is_admin" BOOLEAN NOT NULL DEFAULT false;
|
@ -0,0 +1,19 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "scheduled_note" (
|
||||
"id" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"cw" TEXT,
|
||||
"local_only" BOOLEAN NOT NULL,
|
||||
"visibility" TEXT NOT NULL,
|
||||
"visible_user_ids" TEXT[],
|
||||
"misskey_session_id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "scheduled_note_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scheduled_note_misskey_session_id_key" ON "scheduled_note"("misskey_session_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scheduled_note" ADD CONSTRAINT "scheduled_note_misskey_session_id_fkey" FOREIGN KEY ("misskey_session_id") REFERENCES "misskey_session"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "scheduled_note_misskey_session_id_key";
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scheduled_note" ALTER COLUMN "date" SET DATA TYPE TIMESTAMP;
|
3
packages/backend/prisma/migrations/migration_lock.toml
Normal file
3
packages/backend/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
190
packages/backend/prisma/schema.prisma
Normal file
190
packages/backend/prisma/schema.prisma
Normal file
@ -0,0 +1,190 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
/// Misskey Tools アカウント
|
||||
model Account {
|
||||
/// ID
|
||||
id String @id @default(uuid())
|
||||
/// このアカウントの名前。デフォルトでは @Lutica@mk.shrimpia.network みたいな感じになる
|
||||
name String @db.VarChar
|
||||
/// Misskey Tools API アクセストークン
|
||||
accessToken String @unique @map("access_token") @db.VarChar
|
||||
/// ログイン用Eメールアドレス
|
||||
/// 今後メールによる緊急ログインを実装するとき用
|
||||
email String? @db.VarChar
|
||||
/// ログイン用の一時的なパスワード
|
||||
password String? @db.VarChar
|
||||
/// 管理者かどうか
|
||||
isAdmin Boolean @default(false) @map("is_admin")
|
||||
misskeySessions MisskeySession[]
|
||||
|
||||
@@map("account")
|
||||
}
|
||||
|
||||
/// Tools アカウントに紐づくMisskeyとの連携情報。
|
||||
model MisskeySession {
|
||||
/// ID
|
||||
id String @id @default(uuid())
|
||||
/// ユーザー名
|
||||
username String @db.VarChar
|
||||
/// ホスト名
|
||||
host String @db.VarChar
|
||||
/// Misskey APIトークン
|
||||
token String @db.VarChar
|
||||
/// Misskey APIトークンのバージョン。
|
||||
tokenVersion Int
|
||||
/// Tools アカウントID
|
||||
accountId String @db.VarChar
|
||||
holicAccount HolicAccount?
|
||||
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||||
scheduledNote ScheduledNote[]
|
||||
|
||||
@@unique([username, host])
|
||||
@@map("misskey_session")
|
||||
}
|
||||
|
||||
/// ミス廃アラート アカウント
|
||||
model HolicAccount {
|
||||
/// このアカウントに紐づくMisskeyセッションのID
|
||||
misskeySessionId String @id @map("misskey_session_id")
|
||||
/// アラートをノートとして発行するかどうか
|
||||
alertAsNote Boolean @default(false) @map("alert_as_note")
|
||||
/// アラートを通知欄に表示するかどうか
|
||||
alertAsNotification Boolean @default(true) @map("alert_as_notificaton")
|
||||
/// アラートを投稿する場合のノートの公開範囲
|
||||
noteVisibility String @default("home") @map("note_visibility")
|
||||
/// アラートを投稿する場合にノートを連合なしとするかどうか
|
||||
noteLocalOnly Boolean @default(false) @map("note_local_only")
|
||||
/// アラートのテンプレート文字列
|
||||
template String? @db.VarChar(1024)
|
||||
/// ランキングからBANされているかどうか
|
||||
bannedFromRanking Boolean @default(false) @map("banned_from_ranking")
|
||||
/// 自分の名前をランキングに表示するかどうか
|
||||
rankingVisible Boolean @default(false) @map("ranking_visible")
|
||||
misskeySession MisskeySession @relation(fields: [misskeySessionId], references: [id], onDelete: Cascade)
|
||||
records HolicRecord[]
|
||||
|
||||
@@map("misshai_account")
|
||||
}
|
||||
|
||||
/// ミス廃アラートのログ
|
||||
model HolicRecord {
|
||||
/// ID
|
||||
id String @id @default(uuid())
|
||||
/// このログの日付
|
||||
date DateTime @db.Date
|
||||
/// ノート数
|
||||
notesCount Int @map("notes_count")
|
||||
/// フォロー数
|
||||
followingCount Int @map("following_count")
|
||||
/// フォロワー数
|
||||
followersCount Int @map("followers_count")
|
||||
/// レート値
|
||||
rating Float @default(0) @db.Real
|
||||
/// このログの所有者であるアカウントのID
|
||||
accountId String @map("account_id")
|
||||
account HolicAccount @relation(fields: [accountId], references: [misskeySessionId], onDelete: Cascade)
|
||||
|
||||
@@map("misshai_record")
|
||||
}
|
||||
|
||||
/// 予約されたノート
|
||||
model ScheduledNote {
|
||||
/// ID
|
||||
id String @id @default(uuid())
|
||||
/// このログの日付
|
||||
date DateTime @db.Timestamp(6)
|
||||
/// 予約したノートの本文
|
||||
text String
|
||||
/// 予約したノートのCW
|
||||
cw String?
|
||||
/// 予約したノートを「連合なし」とするかどうか
|
||||
localOnly Boolean @map("local_only")
|
||||
/// 予約したノートの公開範囲
|
||||
visibility String
|
||||
/// 予約したノートが公開範囲「ダイレクト」のときに送信する相手のリスト
|
||||
visibleUserIds String[] @map("visible_user_ids")
|
||||
/// このノートを送信するMisskeyセッションのID
|
||||
misskeySessionId String @map("misskey_session_id")
|
||||
misskeySession MisskeySession @relation(fields: [misskeySessionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("scheduled_note")
|
||||
}
|
||||
|
||||
/// お知らせ
|
||||
model Announcement {
|
||||
/// ID
|
||||
id Int @id(map: "PK_e0ef0550174fd1099a308fd18a0") @default(autoincrement())
|
||||
/// 作成日時
|
||||
createdAt DateTime @db.Timestamp(6)
|
||||
/// タイトル
|
||||
title String @db.VarChar(128)
|
||||
/// 本文
|
||||
body String @db.VarChar(8192)
|
||||
/// いいね数
|
||||
like Int @default(0)
|
||||
|
||||
@@map("announcement")
|
||||
}
|
||||
|
||||
/// 使用済みトークンを管理するテーブル。
|
||||
/// 廃止予定
|
||||
model UsedToken {
|
||||
token String @id(map: "PK_7f2db4c33c33cd6b38e63393fe5") @unique(map: "IDX_7f2db4c33c33cd6b38e63393fe") @db.VarChar
|
||||
|
||||
@@map("used_token")
|
||||
}
|
||||
|
||||
/// Misskey Tools v3以前で使用されていたユーザーモデル。
|
||||
/// 廃止予定。
|
||||
model User {
|
||||
id Int @id(map: "PK_cace4a159ff9f2512dd42373760") @default(autoincrement())
|
||||
username String @db.VarChar
|
||||
host String @db.VarChar
|
||||
token String @db.VarChar
|
||||
prevNotesCount Int @default(0)
|
||||
prevFollowingCount Int @default(0)
|
||||
prevFollowersCount Int @default(0)
|
||||
misshaiToken String @default("") @db.VarChar
|
||||
alertMode AlertMode @default(notification)
|
||||
visibility Visibility @default(home)
|
||||
localOnly Boolean @default(false)
|
||||
remoteFollowersOnly Boolean @default(false)
|
||||
template String? @db.VarChar(1024)
|
||||
prevRating Float @default(0) @db.Real
|
||||
rating Float @default(0) @db.Real
|
||||
bannedFromRanking Boolean @default(false)
|
||||
tokenVersion Int @default(1)
|
||||
useRanking Boolean @default(false)
|
||||
|
||||
@@unique([username, host], map: "IDX_6269eebacdb25de8569298a52a")
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
/// Misskey Tools v3以前で使用していた、アラート送信モードフラグ。
|
||||
/// 廃止予定。
|
||||
enum AlertMode {
|
||||
note
|
||||
notification
|
||||
both
|
||||
nothing
|
||||
|
||||
@@map("user_alertmode_enum")
|
||||
}
|
||||
|
||||
/// Misskey Tools v3以前で使用していた、アラートのノート公開範囲。
|
||||
/// 廃止予定。
|
||||
enum Visibility {
|
||||
public
|
||||
home
|
||||
followers
|
||||
users
|
||||
|
||||
@@map("user_visibility_enum")
|
||||
}
|
@ -11,14 +11,10 @@ html
|
||||
meta(property='og:title' content=title)
|
||||
meta(property='og:description' content=desc)
|
||||
meta(property='og:type' content='website')
|
||||
link(rel="preload" href="https://koruri.chillout.chat/koruri.css")
|
||||
link(rel="preload", href="/assets/otadesign_rounded.woff")
|
||||
link(rel="preload", href="/assets/otadesign_rounded.woff2")
|
||||
link(rel="stylesheet" href="https://koruri.chillout.chat/koruri.css")
|
||||
script(src='https://kit.fontawesome.com/c7ab6eba70.js' crossorigin='anonymous')
|
||||
link(rel="stylesheet", href="/assets/style.css")
|
||||
body
|
||||
#app: .loading Loading...
|
||||
#app
|
||||
|
||||
if token
|
||||
script.
|
||||
@ -34,6 +30,15 @@ html
|
||||
|
||||
if error
|
||||
script.
|
||||
window.__misshaialert = { error: '#{error}' };
|
||||
window.__tools = { error: '#{error}' };
|
||||
|
||||
script(src=`/assets/fe.${version}.js` async defer)
|
||||
script(type='module').
|
||||
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
|
||||
|
||||
script(type='module' src='/vite/@vite/client')
|
||||
script(type='module' src='/vite/src/init.tsx')
|
@ -1,9 +1,11 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { initDb } from './backend/services/db.js';
|
||||
import {config} from './config.js';
|
||||
import { config } from '@/config.js';
|
||||
|
||||
export type { AppRouter } from '@/server/api/index.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const ua = `Mozilla/5.0 MisskeyTools +https://github.com/shrimpia/misskey-tools Node/${process.version} ${config.uaExtra ?? ''}`;
|
||||
|
||||
@ -12,7 +14,5 @@ axios.defaults.headers['Content-Type'] = 'application/json';
|
||||
axios.defaults.validateStatus = (stat) => stat < 500;
|
||||
|
||||
(async () => {
|
||||
await initDb();
|
||||
(await import('./backend/services/worker.js')).default();
|
||||
(await import('./backend/server.js')).default();
|
||||
await import('@/boot/server.js').then(server => server.default());
|
||||
})();
|
10
packages/backend/src/boot/server.ts
Normal file
10
packages/backend/src/boot/server.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { config, meta } from '@/config.js';
|
||||
import { startServer } from '@/server/index.js';
|
||||
|
||||
export default async () => {
|
||||
console.log(`** Misskey Tools ${meta.version} **`);
|
||||
console.log('(C) Shrimpia Network');
|
||||
await startServer();
|
||||
console.log('GET READY!');
|
||||
console.log(`Server URL >> ${config.url}`);
|
||||
};
|
36
packages/backend/src/config.ts
Normal file
36
packages/backend/src/config.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
|
||||
export interface Config {
|
||||
/** Misskey Tools アプリケーションポート */
|
||||
port: number;
|
||||
|
||||
/** Misskey Tools URL */
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* Misskey Tools のユーザーエージェント文字列に追加するテキスト。
|
||||
*/
|
||||
uaExtra: string;
|
||||
|
||||
/**
|
||||
* Redis 設定
|
||||
*/
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const config: Config = Object.freeze(JSON.parse(fs.readFileSync(__dirname + '/../../../config.json', 'utf-8')));
|
||||
|
||||
export const meta: MetaJson = {
|
||||
version: process.env.npm_package_version as string,
|
||||
};
|
||||
|
||||
export type MetaJson = {
|
||||
version: string;
|
||||
};
|
1
packages/backend/src/libs/id.ts
Normal file
1
packages/backend/src/libs/id.ts
Normal file
@ -0,0 +1 @@
|
||||
export { v4 as uuid } from 'uuid';
|
3
packages/backend/src/libs/markdown.ts
Normal file
3
packages/backend/src/libs/markdown.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
export const markdown = new MarkdownIt();
|
29
packages/backend/src/libs/misskey.ts
Normal file
29
packages/backend/src/libs/misskey.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { api as misskey } from 'misskey-js';
|
||||
|
||||
import { ua } from '@/app.js';
|
||||
|
||||
const clientsMap = new Map<string, misskey.APIClient>();
|
||||
|
||||
/**
|
||||
* ホスト名に対応するMisskeyクライアントを取得します。
|
||||
*/
|
||||
export const getMisskey = (host: string) => {
|
||||
let cli = clientsMap.get(host);
|
||||
if (cli != null) return cli;
|
||||
|
||||
cli = new misskey.APIClient({
|
||||
origin: `https://${host}`,
|
||||
fetch: (input, init) => {
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init?.headers ?? []),
|
||||
'User-Agent': ua,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
clientsMap.set(host, cli);
|
||||
return cli;
|
||||
};
|
8
packages/backend/src/libs/prisma.ts
Normal file
8
packages/backend/src/libs/prisma.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Prisma ORMクライアント
|
||||
*/
|
||||
export const prisma = new PrismaClient({
|
||||
log: ['query', 'info', 'warn', 'error'],
|
||||
});
|
5
packages/backend/src/libs/redis.ts
Normal file
5
packages/backend/src/libs/redis.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import IORedis from 'ioredis';
|
||||
|
||||
import { config } from '@/config.js';
|
||||
|
||||
export const connection = new IORedis(config.redis.port ?? 6379, config.redis.host ?? 'localhost');
|
100
packages/backend/src/queue/holic-aggregate.ts
Normal file
100
packages/backend/src/queue/holic-aggregate.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
|
||||
import type { MisskeySession, HolicAccount, HolicRecord } from '@prisma/client';
|
||||
|
||||
import { getMisskey } from '@/libs/misskey.js';
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { connection } from '@/libs/redis.js';
|
||||
import { queues } from '@/queue/index.js';
|
||||
import { format } from '@/services/holic/format.js';
|
||||
import { avg } from '@/utils/avg.js';
|
||||
import { toAcct } from '@/utils/to-acct.js';
|
||||
|
||||
const NAME = 'holicAggregate';
|
||||
|
||||
export type HolicAggregateQueueType = {
|
||||
account: HolicAccount & {
|
||||
misskeySession: MisskeySession;
|
||||
};
|
||||
};
|
||||
|
||||
export const holicAggregateQueue = new Queue<HolicAggregateQueueType>(NAME, { connection });
|
||||
|
||||
export const holicAggregateWorker = new Worker<HolicAggregateQueueType>(NAME, async (job) => {
|
||||
// ステータスをお問い合わせ
|
||||
const { account } = job.data;
|
||||
const { misskeySession: session } = account;
|
||||
const acct = toAcct(session);
|
||||
|
||||
console.log(`[holic] Aggregating for ${acct}`);
|
||||
const data = await getMisskey(session.host).request('i', {}, session.token);
|
||||
if (!('notesCount' in data)) {
|
||||
job.discard();
|
||||
throw new Error(`Endpoint 'i' did not return a detailed user object for @${session.username}@${session.host}.`);
|
||||
}
|
||||
const records1week = await prisma.holicRecord.findMany({
|
||||
where: {
|
||||
accountId: session.id,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
take: 6,
|
||||
});
|
||||
|
||||
// レート計算
|
||||
const rating = records1week.length > 0 ? avg([
|
||||
...records1week.map(r => r.notesCount),
|
||||
data.notesCount,
|
||||
]) : 0;
|
||||
|
||||
console.log(`[holic] RATING of ${acct}: ${rating}`);
|
||||
|
||||
// 今日分のデータ作成
|
||||
const today = await prisma.holicRecord.create({
|
||||
data: {
|
||||
date: new Date(),
|
||||
notesCount: data.notesCount,
|
||||
followingCount: data.followingCount,
|
||||
followersCount: data.followersCount,
|
||||
accountId: session.id,
|
||||
rating,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const yesterday: HolicRecord = records1week[0] ?? today;
|
||||
|
||||
const text = format({
|
||||
today,
|
||||
yesterday,
|
||||
account,
|
||||
session,
|
||||
});
|
||||
|
||||
if (account.alertAsNote) {
|
||||
queues.holicNoteQueue.add(acct, {
|
||||
account,
|
||||
session,
|
||||
text,
|
||||
}, {
|
||||
backoff: 5,
|
||||
delay: 1000 * 60,
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (account.alertAsNotification) {
|
||||
queues.holicNotificationQueue.add(acct, {
|
||||
account,
|
||||
session,
|
||||
text,
|
||||
}, {
|
||||
backoff: 5,
|
||||
delay: 1000 * 60,
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
}, {
|
||||
connection,
|
||||
});
|
30
packages/backend/src/queue/holic-cron.ts
Normal file
30
packages/backend/src/queue/holic-cron.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
|
||||
import type { BulkJobOptions } from 'bullmq';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { connection } from '@/libs/redis.js';
|
||||
import { queues } from '@/queue/index.js';
|
||||
import { toAcct } from '@/utils/to-acct.js';
|
||||
|
||||
const NAME = 'holicCron';
|
||||
|
||||
export const holicCronQueue = new Queue(NAME, { connection });
|
||||
|
||||
export const holicCronWorker = new Worker(NAME, async () => {
|
||||
const jobData = (await prisma.holicAccount.findMany({
|
||||
include: {
|
||||
misskeySession: true,
|
||||
},
|
||||
})).map(account => ({
|
||||
name: toAcct(account.misskeySession),
|
||||
data: { account },
|
||||
opts: {
|
||||
backoff: 5,
|
||||
delay: 3000,
|
||||
removeOnComplete: true,
|
||||
} as BulkJobOptions,
|
||||
}));
|
||||
|
||||
queues.holicAggregateQueue.addBulk(jobData);
|
||||
}, { connection });
|
51
packages/backend/src/queue/holic-note.ts
Normal file
51
packages/backend/src/queue/holic-note.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import * as misskey from 'misskey-js';
|
||||
|
||||
import type { MisskeySession, HolicAccount } from '@prisma/client';
|
||||
|
||||
import { getMisskey } from '@/libs/misskey.js';
|
||||
import { connection } from '@/libs/redis.js';
|
||||
import { isVisibility } from '@/utils/is-visibility.js';
|
||||
import { toAcct } from '@/utils/to-acct.js';
|
||||
|
||||
const NAME = 'holicNote';
|
||||
|
||||
export type HolicNoteQueueType = {
|
||||
session: MisskeySession;
|
||||
account: HolicAccount;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const holicNoteQueue = new Queue<HolicNoteQueueType>(NAME, { connection });
|
||||
|
||||
export const holicNoteWorker = new Worker<HolicNoteQueueType>(NAME, async (job) => {
|
||||
const { session, account, text } = job.data;
|
||||
|
||||
console.log(`[holic] Processing note job for ${toAcct(session)}`);
|
||||
|
||||
const api = getMisskey(session.host);
|
||||
|
||||
if (!isVisibility(account.noteVisibility)) {
|
||||
// 公開範囲の値がおかしい場合は処理しない
|
||||
// TODO 警告をログに流す
|
||||
console.warn(`The scheduled note id:${job.data} has wrong visibility ${account.noteVisibility}. Ignored.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.request('notes/create', {
|
||||
text,
|
||||
visibility: account.noteVisibility,
|
||||
localOnly: account.noteLocalOnly,
|
||||
}, session.token);
|
||||
} catch (e) {
|
||||
console.log(`[holic] Failed to create a note: ${e}`);
|
||||
if (!misskey.api.isAPIError(e)) throw e;
|
||||
if (e.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
// delay 1h
|
||||
holicNoteWorker.rateLimit(1000 * 60 * 60);
|
||||
throw Worker.RateLimitError();
|
||||
}
|
||||
}
|
||||
|
||||
}, { connection });
|
30
packages/backend/src/queue/holic-notification.ts
Normal file
30
packages/backend/src/queue/holic-notification.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
|
||||
import type { MisskeySession, HolicAccount } from '@prisma/client';
|
||||
|
||||
import { getMisskey } from '@/libs/misskey.js';
|
||||
import { connection } from '@/libs/redis.js';
|
||||
import { toAcct } from '@/utils/to-acct';
|
||||
|
||||
const NAME = 'holicNotification';
|
||||
|
||||
export type HolicNotificationQueueType = {
|
||||
session: MisskeySession;
|
||||
account: HolicAccount;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const holicNotificationQueue = new Queue<HolicNotificationQueueType>(NAME, { connection });
|
||||
|
||||
export const holicNotificationWorker = new Worker<HolicNotificationQueueType>(NAME, async (job) => {
|
||||
const { session, text } = job.data;
|
||||
console.log(`[holic] Processing note job for ${toAcct(session)}`);
|
||||
|
||||
const api = getMisskey(session.host);
|
||||
await api.request('notifications/create', {
|
||||
header: 'Misskey Tools',
|
||||
icon: 'https://i.imgur.com/B991yTl.png',
|
||||
body: text,
|
||||
}, session.token);
|
||||
|
||||
}, { connection });
|
21
packages/backend/src/queue/index.ts
Normal file
21
packages/backend/src/queue/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { holicAggregateQueue, holicAggregateWorker } from './holic-aggregate.js';
|
||||
import { holicCronQueue, holicCronWorker } from './holic-cron.js';
|
||||
import { holicNoteQueue, holicNoteWorker } from './holic-note.js';
|
||||
import { holicNotificationQueue, holicNotificationWorker } from './holic-notification.js';
|
||||
import { noteSchedulerQueue, noteSchedulerWorker } from './note-scheduler.js';
|
||||
|
||||
export const queues = {
|
||||
noteSchedulerQueue,
|
||||
holicAggregateQueue,
|
||||
holicCronQueue,
|
||||
holicNoteQueue,
|
||||
holicNotificationQueue,
|
||||
};
|
||||
|
||||
export const workers = {
|
||||
noteSchedulerWorker,
|
||||
holicAggregateWorker,
|
||||
holicCronWorker,
|
||||
holicNoteWorker,
|
||||
holicNotificationWorker,
|
||||
};
|
47
packages/backend/src/queue/note-scheduler.ts
Normal file
47
packages/backend/src/queue/note-scheduler.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
|
||||
import { getMisskey } from '@/libs/misskey.js';
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { connection } from '@/libs/redis.js';
|
||||
import { isVisibility } from '@/utils/is-visibility.js';
|
||||
|
||||
const NAME = 'noteScheduler';
|
||||
|
||||
export type NoteSchedulerQueueType = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const noteSchedulerQueue = new Queue<NoteSchedulerQueueType>(NAME, { connection });
|
||||
|
||||
export const noteSchedulerWorker = new Worker<NoteSchedulerQueueType>(NAME, async (job) => {
|
||||
const scheduledNote = await prisma.scheduledNote.findUnique({
|
||||
where: { id: job.data.id },
|
||||
include: {
|
||||
misskeySession: {
|
||||
select: {
|
||||
token: true,
|
||||
host: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (scheduledNote == null) {
|
||||
// データベース上に存在しない場合は処理しない
|
||||
// TODO 警告をログに流す
|
||||
console.warn(`The scheduled note id:${job.data} is not found. Ignored.`);
|
||||
return;
|
||||
}
|
||||
if (!isVisibility(scheduledNote.visibility)) {
|
||||
// 公開範囲の値がおかしい場合は処理しない
|
||||
// TODO 警告をログに流す
|
||||
console.warn(`The scheduled note id:${job.data} has wrong visibility ${scheduledNote.visibility}. Ignored.`);
|
||||
return;
|
||||
}
|
||||
await getMisskey(scheduledNote.misskeySession.host).request('notes/create', {
|
||||
cw: scheduledNote.cw,
|
||||
text: scheduledNote.text,
|
||||
visibility: scheduledNote.visibility,
|
||||
localOnly: scheduledNote.localOnly,
|
||||
visibleUserIds: scheduledNote.visibleUserIds,
|
||||
}, scheduledNote.misskeySession.token);
|
||||
}, { connection });
|
47
packages/backend/src/scripts/migrate-legacy-user.ts
Normal file
47
packages/backend/src/scripts/migrate-legacy-user.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { normalize } from '@/utils/visibility.js';
|
||||
|
||||
/**
|
||||
* user テーブルを Account, MisskeySession, HolicAccount テーブルに移行します。
|
||||
*/
|
||||
export const migrateLegacyUser = async () => {
|
||||
if ((await prisma.user.count()) === 0) return;
|
||||
console.log('System will migrate legacy `user` table into `accounts` table.');
|
||||
const users = await prisma.user.findMany();
|
||||
for (const user of users) {
|
||||
const account = await prisma.account.create({
|
||||
data: {
|
||||
name: user.username,
|
||||
accessToken: user.misshaiToken,
|
||||
},
|
||||
});
|
||||
const session = await prisma.misskeySession.create({
|
||||
data: {
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
token: user.token,
|
||||
tokenVersion: user.tokenVersion,
|
||||
accountId: account.id,
|
||||
},
|
||||
});
|
||||
await prisma.holicAccount.create({
|
||||
data: {
|
||||
misskeySessionId: session.id,
|
||||
alertAsNote: user.alertMode === 'note' || user.alertMode === 'both',
|
||||
alertAsNotification: user.alertMode === 'notification' || user.alertMode === 'both',
|
||||
noteVisibility: normalize(user.visibility),
|
||||
noteLocalOnly: user.localOnly,
|
||||
template: user.template,
|
||||
bannedFromRanking: user.bannedFromRanking,
|
||||
rankingVisible: user.useRanking,
|
||||
},
|
||||
});
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
console.log(`Processed for ${user.username}@${user.host}`);
|
||||
}
|
||||
console.log('All done.');
|
||||
};
|
17
packages/backend/src/server/api/dto/account.ts
Normal file
17
packages/backend/src/server/api/dto/account.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Account } from '@prisma/client';
|
||||
|
||||
export const accountDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
isAdmin: z.boolean(),
|
||||
}).strict();
|
||||
|
||||
export type AccountDto = z.infer<typeof accountDtoSchema>;
|
||||
|
||||
export const toAccountDto = (a: Account): AccountDto => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
isAdmin: a.isAdmin,
|
||||
});
|
19
packages/backend/src/server/api/dto/announcement.ts
Normal file
19
packages/backend/src/server/api/dto/announcement.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const announcementListItemDtoSchema = z.object({
|
||||
id: z.number(),
|
||||
createdAt: z.date(),
|
||||
title: z.string(),
|
||||
}).strict();
|
||||
|
||||
export type AnnouncementListItemDto = z.infer<typeof announcementListItemDtoSchema>;
|
||||
|
||||
export const announcementDtoSchema = z.object({
|
||||
id: z.number(),
|
||||
createdAt: z.date(),
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
like: z.number(),
|
||||
}).strict();
|
||||
|
||||
export type AnnouncementDto = z.infer<typeof announcementDtoSchema>;
|
25
packages/backend/src/server/api/dto/holic-account.ts
Normal file
25
packages/backend/src/server/api/dto/holic-account.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { HolicAccount } from '@prisma/client';
|
||||
|
||||
export const holicAccountDtoSchema = z.object({
|
||||
misskeySessionId: z.string(),
|
||||
alertAsNote: z.boolean(),
|
||||
alertAsNotification: z.boolean(),
|
||||
noteVisibility: z.string(),
|
||||
noteLocalOnly: z.boolean(),
|
||||
template: z.string().nullable(),
|
||||
rankingVisible: z.boolean(),
|
||||
}).strict();
|
||||
|
||||
export type HolicAccountDto = z.infer<typeof holicAccountDtoSchema>;
|
||||
|
||||
export const toHolicAccountDto = (a: HolicAccount): HolicAccountDto => ({
|
||||
misskeySessionId: a.misskeySessionId,
|
||||
alertAsNote: a.alertAsNote,
|
||||
alertAsNotification: a.alertAsNotification,
|
||||
noteVisibility: a.noteVisibility,
|
||||
noteLocalOnly: a.noteLocalOnly,
|
||||
template: a.template,
|
||||
rankingVisible: a.rankingVisible,
|
||||
});
|
9
packages/backend/src/server/api/dto/meta.ts
Normal file
9
packages/backend/src/server/api/dto/meta.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { currentTokenVersion } from 'tools-shared/dist/const.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const metaDtoSchema = z.object({
|
||||
version: z.string(),
|
||||
currentTokenVersion: z.literal(currentTokenVersion),
|
||||
}).strict();
|
||||
|
||||
export type MetaDto = z.infer<typeof metaDtoSchema>;
|
17
packages/backend/src/server/api/dto/misskey-session.ts
Normal file
17
packages/backend/src/server/api/dto/misskey-session.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { MisskeySession } from '@prisma/client';
|
||||
|
||||
export const misskeySessionDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
host: z.string(),
|
||||
}).strict();
|
||||
|
||||
export type MisskeySessionDto = z.infer<typeof misskeySessionDtoSchema>;
|
||||
|
||||
export const toMisskeySessionDto = (s: MisskeySession): MisskeySessionDto => ({
|
||||
id: s.id,
|
||||
username: s.username,
|
||||
host: s.host,
|
||||
});
|
27
packages/backend/src/server/api/dto/scheduled-note.ts
Normal file
27
packages/backend/src/server/api/dto/scheduled-note.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { ScheduledNote } from '@prisma/client';
|
||||
|
||||
export const scheduledNoteDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
date: z.date(),
|
||||
text: z.string(),
|
||||
cw: z.string().nullable(),
|
||||
localOnly: z.boolean(),
|
||||
visibility: z.string(),
|
||||
visibleUserIds: z.array(z.string()),
|
||||
misskeySessionId: z.string(),
|
||||
}).strict();
|
||||
|
||||
export type ScheduledNoteDto = z.infer<typeof scheduledNoteDtoSchema>;
|
||||
|
||||
export const toScheduledNoteDto = (s: ScheduledNote): ScheduledNoteDto => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
text: s.text,
|
||||
cw: s.cw,
|
||||
localOnly: s.localOnly,
|
||||
visibility: s.visibility,
|
||||
visibleUserIds: s.visibleUserIds,
|
||||
misskeySessionId: s.misskeySessionId,
|
||||
});
|
17
packages/backend/src/server/api/index.ts
Normal file
17
packages/backend/src/server/api/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { holicRouter } from './routers/holic';
|
||||
|
||||
import { accountRouter } from '@/server/api/routers/account.js';
|
||||
import { announcementsRouter } from '@/server/api/routers/announcements.js';
|
||||
import { metaRouter } from '@/server/api/routers/meta.js';
|
||||
import { noteSchedulerRouter } from '@/server/api/routers/note-scheduler.js';
|
||||
import { router } from '@/server/api/trpc.js';
|
||||
|
||||
export const appRouter = router({
|
||||
meta: metaRouter,
|
||||
account: accountRouter,
|
||||
announcements: announcementsRouter,
|
||||
noteScheduler: noteSchedulerRouter,
|
||||
holic: holicRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
17
packages/backend/src/server/api/procedures/admin.ts
Normal file
17
packages/backend/src/server/api/procedures/admin.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { middleware, procedure } from '@/server/api/trpc.js';
|
||||
|
||||
const isAdmin = middleware(({ next, ctx }) => {
|
||||
if (!ctx.account?.isAdmin) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.token,
|
||||
account: ctx.account,
|
||||
},
|
||||
});
|
||||
});
|
||||
// you can reuse this for any procedure
|
||||
export const adminProcedure = procedure.use(isAdmin);
|
18
packages/backend/src/server/api/procedures/session.ts
Normal file
18
packages/backend/src/server/api/procedures/session.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { middleware, procedure } from '@/server/api/trpc.js';
|
||||
|
||||
const hasSession = middleware(({ next, ctx }) => {
|
||||
const { account } = ctx;
|
||||
if (!account) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.token,
|
||||
account: account,
|
||||
},
|
||||
});
|
||||
});
|
||||
// you can reuse this for any procedure
|
||||
export const sessionProcedure = procedure.use(hasSession);
|
38
packages/backend/src/server/api/routers/account.ts
Normal file
38
packages/backend/src/server/api/routers/account.ts
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@/libs/prisma';
|
||||
import { accountDtoSchema, toAccountDto } from '@/server/api/dto/account';
|
||||
import { misskeySessionDtoSchema } from '@/server/api/dto/misskey-session';
|
||||
import { sessionProcedure } from '@/server/api/procedures/session.js';
|
||||
import { router } from '@/server/api/trpc.js';
|
||||
|
||||
export const accountRouter = router({
|
||||
getMyself: sessionProcedure
|
||||
.output(accountDtoSchema)
|
||||
.query(({ ctx }) => {
|
||||
return toAccountDto(ctx.account);
|
||||
}),
|
||||
getMisskeySessions: sessionProcedure
|
||||
.output(z.array(misskeySessionDtoSchema))
|
||||
.query(async ({ ctx }) => {
|
||||
const sessions = await prisma.misskeySession.findMany({
|
||||
where: { accountId: ctx.account.id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
host: true,
|
||||
},
|
||||
});
|
||||
return sessions;
|
||||
}),
|
||||
changeName: sessionProcedure
|
||||
.input(z.string())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await prisma.account.update({
|
||||
where: { id: ctx.account.id },
|
||||
data: { name: input },
|
||||
select: { id: true },
|
||||
});
|
||||
}),
|
||||
});
|
62
packages/backend/src/server/api/routers/announcements.ts
Normal file
62
packages/backend/src/server/api/routers/announcements.ts
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { sessionProcedure } from '../procedures/session';
|
||||
|
||||
import { prisma } from '@/libs/prisma';
|
||||
import { announcementDtoSchema, announcementListItemDtoSchema } from '@/server/api/dto/announcement';
|
||||
import { procedure, router } from '@/server/api/trpc.js';
|
||||
|
||||
|
||||
export const announcementsRouter = router({
|
||||
getAll: procedure
|
||||
.output(z.array(announcementListItemDtoSchema))
|
||||
.query(async () => {
|
||||
return await prisma.announcement.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}),
|
||||
get: procedure
|
||||
.output(announcementDtoSchema)
|
||||
.input(z.number())
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
return await prisma.announcement.findUniqueOrThrow({
|
||||
where: { id: input },
|
||||
});
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
}),
|
||||
like: sessionProcedure
|
||||
.input(z.number())
|
||||
.output(z.number())
|
||||
.mutation(async ({ input }) => {
|
||||
const announcement = await prisma.announcement.findUnique({
|
||||
where: { id: input },
|
||||
select: { id: true, like: true, createdAt: true },
|
||||
});
|
||||
if (announcement == null) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
const { like } = await prisma.announcement.update({
|
||||
where: { id: input },
|
||||
data: { like: announcement.like + 1 },
|
||||
select: { like: true },
|
||||
});
|
||||
|
||||
return like;
|
||||
}),
|
||||
});
|
160
packages/backend/src/server/api/routers/holic.ts
Normal file
160
packages/backend/src/server/api/routers/holic.ts
Normal file
@ -0,0 +1,160 @@
|
||||
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { adminProcedure } from '../procedures/admin';
|
||||
|
||||
import { prisma } from '@/libs/prisma';
|
||||
import { queues } from '@/queue';
|
||||
import { holicAccountDtoSchema, toHolicAccountDto } from '@/server/api/dto/holic-account.js';
|
||||
import { sessionProcedure } from '@/server/api/procedures/session.js';
|
||||
import { router } from '@/server/api/trpc.js';
|
||||
import { getSession } from '@/services/sessions/get-session.js';
|
||||
|
||||
export const holicRouter = router({
|
||||
getAccount: sessionProcedure
|
||||
.output(holicAccountDtoSchema.nullable())
|
||||
.input(z.object({
|
||||
sessionId: z.string(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const accountId = ctx.account.id;
|
||||
const session = await getSession(input.sessionId, accountId);
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
||||
const holicAccount = await prisma.holicAccount.findUnique({
|
||||
where: { misskeySessionId: session.id },
|
||||
});
|
||||
if (!holicAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toHolicAccountDto(holicAccount);
|
||||
}),
|
||||
|
||||
createAccount: sessionProcedure
|
||||
.output(holicAccountDtoSchema)
|
||||
.input(z.object({
|
||||
sessionId: z.string(),
|
||||
alertAsNote: z.boolean(),
|
||||
alertAsNotification: z.boolean(),
|
||||
noteVisibility: z.string(),
|
||||
noteLocalOnly: z.boolean(),
|
||||
template: z.string().nullable(),
|
||||
rankingVisible: z.boolean(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const accountId = ctx.account.id;
|
||||
const session = await getSession(input.sessionId, accountId);
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
const isExists = await prisma.holicAccount.findUnique({
|
||||
where: { misskeySessionId: session.id },
|
||||
select: { misskeySessionId: true },
|
||||
});
|
||||
|
||||
if (isExists) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
});
|
||||
}
|
||||
|
||||
const account = await prisma.holicAccount.create({
|
||||
data: {
|
||||
misskeySessionId: session.id,
|
||||
alertAsNote: input.alertAsNote,
|
||||
alertAsNotification: input.alertAsNotification,
|
||||
rankingVisible: input.rankingVisible,
|
||||
noteVisibility: input.noteVisibility,
|
||||
noteLocalOnly: input.noteLocalOnly,
|
||||
template: input.template === '' ? null : input.template,
|
||||
},
|
||||
});
|
||||
return toHolicAccountDto(account);
|
||||
}),
|
||||
|
||||
updateAccount: sessionProcedure
|
||||
.output(holicAccountDtoSchema)
|
||||
.input(z.object({
|
||||
sessionId: z.string(),
|
||||
alertAsNote: z.boolean().optional(),
|
||||
alertAsNotification: z.boolean().optional(),
|
||||
noteVisibility: z.string().optional(),
|
||||
noteLocalOnly: z.boolean().optional(),
|
||||
template: z.string().nullable().optional(),
|
||||
rankingVisible: z.boolean().optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const accountId = ctx.account.id;
|
||||
const session = await getSession(input.sessionId, accountId);
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
const isExists = await prisma.holicAccount.findUnique({
|
||||
where: { misskeySessionId: session.id },
|
||||
select: { misskeySessionId: true },
|
||||
});
|
||||
|
||||
if (!isExists) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
||||
const account = await prisma.holicAccount.update({
|
||||
where: { misskeySessionId: session.id },
|
||||
data: {
|
||||
alertAsNote: input.alertAsNote,
|
||||
alertAsNotification: input.alertAsNotification,
|
||||
rankingVisible: input.rankingVisible,
|
||||
noteVisibility: input.noteVisibility,
|
||||
noteLocalOnly: input.noteLocalOnly,
|
||||
template: input.template === '' ? null : input.template,
|
||||
},
|
||||
});
|
||||
return toHolicAccountDto(account);
|
||||
}),
|
||||
|
||||
deleteAccount: sessionProcedure
|
||||
.output(z.void())
|
||||
.input(z.object({
|
||||
sessionId: z.string(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const accountId = ctx.account.id;
|
||||
const session = await getSession(input.sessionId, accountId);
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
const isExists = await prisma.holicAccount.findUnique({
|
||||
where: { misskeySessionId: session.id },
|
||||
select: { misskeySessionId: true },
|
||||
});
|
||||
|
||||
if (!isExists) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.holicAccount.delete({
|
||||
where: { misskeySessionId: session.id },
|
||||
});
|
||||
}),
|
||||
|
||||
adminForceRunAll: adminProcedure
|
||||
.mutation(async () => {
|
||||
await queues.holicCronQueue.add('force-run', null);
|
||||
}),
|
||||
});
|
14
packages/backend/src/server/api/routers/meta.ts
Normal file
14
packages/backend/src/server/api/routers/meta.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { currentTokenVersion } from 'tools-shared/dist/const.js';
|
||||
|
||||
import { meta } from '@/config';
|
||||
import { metaDtoSchema } from '@/server/api/dto/meta';
|
||||
import { procedure, router } from '@/server/api/trpc';
|
||||
|
||||
export const metaRouter = router({
|
||||
get: procedure
|
||||
.output(metaDtoSchema)
|
||||
.query(() => ({
|
||||
version: meta.version,
|
||||
currentTokenVersion,
|
||||
})),
|
||||
});
|
47
packages/backend/src/server/api/routers/note-scheduler.ts
Normal file
47
packages/backend/src/server/api/routers/note-scheduler.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { noteVisibilities } from 'misskey-js';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { scheduledNoteDtoSchema, toScheduledNoteDto } from '@/server/api/dto/scheduled-note';
|
||||
import { sessionProcedure } from '@/server/api/procedures/session.js';
|
||||
import { router } from '@/server/api/trpc.js';
|
||||
import { createScheduledNote } from '@/services/note-scheduler/create';
|
||||
import { deleteScheduledNote } from '@/services/note-scheduler/delete';
|
||||
import { getScheduledNotes } from '@/services/note-scheduler/get';
|
||||
import { getSession } from '@/services/sessions/get-session.js';
|
||||
|
||||
export const noteSchedulerRouter = router({
|
||||
create: sessionProcedure
|
||||
.input(z.object({
|
||||
sessionId: z.string(),
|
||||
note: z.object({
|
||||
text: z.string(),
|
||||
cw: z.string().nullable(),
|
||||
localOnly: z.boolean(),
|
||||
visibility: z.enum(noteVisibilities),
|
||||
visibleUserIds: z.array(z.string()).default([]),
|
||||
}),
|
||||
timestamp: z.coerce.date(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const session = await getSession(input.sessionId, ctx.account.id);
|
||||
if (session == null) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'No such session.',
|
||||
});
|
||||
}
|
||||
await createScheduledNote(session, input.note, input.timestamp);
|
||||
}),
|
||||
list: sessionProcedure
|
||||
.output(z.array(scheduledNoteDtoSchema))
|
||||
.query(async ({ ctx }) => {
|
||||
const notes = await getScheduledNotes(ctx.account);
|
||||
return notes.map(n => toScheduledNoteDto(n));
|
||||
}),
|
||||
delete: sessionProcedure
|
||||
.input(z.string())
|
||||
.mutation(async ({ ctx, input: id }) => {
|
||||
await deleteScheduledNote(id, ctx.account);
|
||||
}),
|
||||
});
|
23
packages/backend/src/server/api/trpc.ts
Normal file
23
packages/backend/src/server/api/trpc.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { initTRPC } from '@trpc/server';
|
||||
|
||||
import type { inferAsyncReturnType } from '@trpc/server';
|
||||
import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
|
||||
|
||||
import { getAccountByAccessToken } from '@/services/accounts/get-account-by-access-token.js';
|
||||
|
||||
export async function createContext({ req }: CreateFastifyContextOptions) {
|
||||
const tokens = req.headers.authorization?.split(' ');
|
||||
const token = tokens?.length === 2 && tokens[0] === 'Bearer' ? tokens[1] : null;
|
||||
const account = token ? await getAccountByAccessToken(token) : null;
|
||||
return { token, account };
|
||||
}
|
||||
|
||||
export type Context = inferAsyncReturnType<typeof createContext>;
|
||||
|
||||
const t = initTRPC.context<Context>().create();
|
||||
|
||||
export const {
|
||||
router,
|
||||
procedure,
|
||||
middleware,
|
||||
} = t;
|
2
packages/backend/src/server/cache.ts
Normal file
2
packages/backend/src/server/cache.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const sessionHostCache: Record<string, string> = {};
|
||||
export const tokenSecretCache: Record<string, string> = {};
|
18
packages/backend/src/server/controllers/announcements.ts
Normal file
18
packages/backend/src/server/controllers/announcements.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import striptags from 'striptags';
|
||||
|
||||
import type { RouteHandler } from 'fastify';
|
||||
|
||||
import { markdown } from '@/libs/markdown.js';
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
/**
|
||||
* お知らせのogpタグをSSRし、フロントエンドを返します。
|
||||
*/
|
||||
export const announcementsController: RouteHandler<{Params: {id: string}}> = async (req, reply) => {
|
||||
const a = await prisma.announcement.findUnique({ where: { id: Number(req.params.id) } });
|
||||
const stripped = striptags(markdown.render(a?.body ?? '').replace(/\n/g, ' '));
|
||||
await reply.view('frontend', a ? {
|
||||
t: a.title,
|
||||
d: stripped.length > 80 ? stripped.substring(0, 80) + '…' : stripped,
|
||||
} : {});
|
||||
};
|
70
packages/backend/src/server/controllers/auth-misskey.ts
Normal file
70
packages/backend/src/server/controllers/auth-misskey.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { misskeyAppInfo } from 'tools-shared/dist/const.js';
|
||||
|
||||
import type { RouteHandler } from 'fastify';
|
||||
|
||||
import { config } from '@/config.js';
|
||||
import { uuid } from '@/libs/id.js';
|
||||
import { getMisskey } from '@/libs/misskey.js';
|
||||
import { sessionHostCache, tokenSecretCache } from '@/server/cache.js';
|
||||
import { die } from '@/server/utils/die.js';
|
||||
|
||||
const miAuth = (host: string) => {
|
||||
const { name, permission } = misskeyAppInfo;
|
||||
const callback = encodeURI(`${config.url}/miauth`);
|
||||
const session = uuid();
|
||||
sessionHostCache[session] = host;
|
||||
return `https://${host}/miauth/${session}?name=${encodeURI(name)}&callback=${encodeURI(callback)}&permission=${encodeURI(permission.join(','))}`;
|
||||
};
|
||||
|
||||
const legacyAuth = async (host: string) => {
|
||||
const misskey = getMisskey(host);
|
||||
const { name, permission, description } = misskeyAppInfo;
|
||||
const callbackUrl = encodeURI(`${config.url}/legacy-auth`);
|
||||
const { secret } = await misskey.request('app/create', {
|
||||
name, description, permission, callbackUrl,
|
||||
});
|
||||
const { token, url } = await misskey.request('auth/session/generate', {
|
||||
appSecret: secret,
|
||||
});
|
||||
sessionHostCache[token] = host;
|
||||
tokenSecretCache[token] = secret;
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Misskeyサーバーに接続するコントローラーです。
|
||||
*/
|
||||
export const authMisskeyController: RouteHandler<{Querystring: {host: string}}> = async (req, reply) => {
|
||||
let host = req.query.host;
|
||||
if (!host) {
|
||||
await die(reply, 'invalidParamater');
|
||||
return;
|
||||
}
|
||||
// http://, https://を潰す
|
||||
host = host.trim().replace(/^https?:\/\//g, '').replace(/\/+/g, '');
|
||||
|
||||
try {
|
||||
const meta = await getMisskey(host).request('meta', { detail: true });
|
||||
|
||||
if (typeof meta !== 'object') {
|
||||
await die(reply, 'other');
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// 環境によってはアクセスしたドメインとMisskeyにおけるhostが異なるケースがある
|
||||
// そういったインスタンスにおいてアカウントの不整合が生じるため、
|
||||
// APIから戻ってきたホスト名を正しいものとして、改めて正規化する
|
||||
host = meta.uri.replace(/^https?:\/\//g, '').replace(/\/+/g, '').trim();
|
||||
|
||||
if (meta.features.miauth) {
|
||||
reply.redirect(miAuth(host));
|
||||
} else {
|
||||
reply.redirect(await legacyAuth(host));
|
||||
}
|
||||
} catch(e) {
|
||||
if (!(e instanceof Error && e.name === 'Error')) throw e;
|
||||
await die(reply, 'hostNotFound');
|
||||
}
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import type { RouteHandler } from 'fastify';
|
||||
import type { UserDetailed } from 'misskey-js/built/entities.js';
|
||||
|
||||
import { getMisskey } from '@/libs/misskey.js';
|
||||
import { tokenSecretCache, sessionHostCache } from '@/server/cache.js';
|
||||
import { die } from '@/server/utils/die.js';
|
||||
import { processLogin } from '@/services/sessions/process-login.js';
|
||||
|
||||
/**
|
||||
* Misskeyに旧型認証を飛ばしたときに返ってくるコールバックのハンドラーです。
|
||||
*/
|
||||
export const callbackLegacyAuthController: RouteHandler<{Querystring: {token: string}}> = async (req, reply) => {
|
||||
const token = req.query.token as string | undefined;
|
||||
if (!token) {
|
||||
await die(reply, 'tokenRequired');
|
||||
return;
|
||||
}
|
||||
const host = sessionHostCache[token];
|
||||
delete sessionHostCache[token];
|
||||
if (!host) {
|
||||
await die(reply);
|
||||
return;
|
||||
}
|
||||
const appSecret = tokenSecretCache[token];
|
||||
delete tokenSecretCache[token];
|
||||
if (!appSecret) {
|
||||
await die(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken: misskeyToken, user } = await getMisskey(host).request('auth/session/userkey', {
|
||||
appSecret, token,
|
||||
});
|
||||
|
||||
const i = crypto.createHash('sha256').update(misskeyToken + appSecret, 'utf8').digest('hex');
|
||||
const accessToken = await processLogin(user as UserDetailed, host, i);
|
||||
|
||||
await reply.view('frontend', { token: accessToken });
|
||||
};
|
41
packages/backend/src/server/controllers/callback-miauth.ts
Normal file
41
packages/backend/src/server/controllers/callback-miauth.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import type { RouteHandler } from 'fastify';
|
||||
|
||||
import { sessionHostCache } from '@/server/cache.js';
|
||||
import { die } from '@/server/utils/die.js';
|
||||
import { processLogin } from '@/services/sessions/process-login.js';
|
||||
|
||||
/**
|
||||
* MisskeyにMiAuth認証を飛ばしたときに返ってくるコールバックのハンドラーです。
|
||||
*/
|
||||
export const callbackMiauthController: RouteHandler<{Querystring: {session: string}}> = async (req, reply) => {
|
||||
const session = req.query.session as string | undefined;
|
||||
if (!session) {
|
||||
await die(reply, 'sessionRequired');
|
||||
return;
|
||||
}
|
||||
const host = sessionHostCache[session];
|
||||
delete sessionHostCache[session];
|
||||
if (!host) {
|
||||
await die(reply);
|
||||
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;
|
||||
|
||||
if (!token || !user) {
|
||||
await die(reply);
|
||||
if (!token) console.error('token is null or undefined');
|
||||
if (!user) console.error('user is null or undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Try to get a Misskey access token from ${host} with session ${session}...`);
|
||||
const accessToken = await processLogin(user, host, token);
|
||||
await reply.view('frontend', { token: accessToken });
|
||||
};
|
8
packages/backend/src/server/controllers/frontend.ts
Normal file
8
packages/backend/src/server/controllers/frontend.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { RouteHandler } from 'fastify';
|
||||
|
||||
/**
|
||||
* フロントエンドを返します。
|
||||
*/
|
||||
export const frontendController: RouteHandler = async (_, reply) => {
|
||||
await reply.view('frontend');
|
||||
};
|
8
packages/backend/src/server/controllers/rescue.ts
Normal file
8
packages/backend/src/server/controllers/rescue.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { RouteHandler } from 'fastify';
|
||||
|
||||
/**
|
||||
* フロントエンドのlocalStorageを初期化するレスキューページを配信します。
|
||||
*/
|
||||
export const rescueController: RouteHandler = async (_, reply) => {
|
||||
await reply.view('rescue');
|
||||
};
|
20
packages/backend/src/server/controllers/vite.ts
Normal file
20
packages/backend/src/server/controllers/vite.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { RouteHandler } from 'fastify';
|
||||
|
||||
/**
|
||||
* Vite アセットを返します。
|
||||
*/
|
||||
export const viteController: RouteHandler = async (req, reply) => {
|
||||
// リクエストされたファイルのパス
|
||||
const path = req.url.slice('/vite'.length);
|
||||
|
||||
// 本番環境
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
reply.statusCode = 404;
|
||||
return {
|
||||
error: '未実装',
|
||||
};
|
||||
}
|
||||
|
||||
// 開発環境
|
||||
reply.redirect(302, 'http://localhost:5173' + path);
|
||||
};
|
59
packages/backend/src/server/index.ts
Normal file
59
packages/backend/src/server/index.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
import fastifyView from '@fastify/view';
|
||||
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
||||
import { fastify } from 'fastify';
|
||||
import pug from 'pug';
|
||||
|
||||
import { config, meta } from '@/config.js';
|
||||
import { queues } from '@/queue/index.js';
|
||||
import { appRouter } from '@/server/api/index.js';
|
||||
import { createContext } from '@/server/api/trpc.js';
|
||||
import { announcementsController } from '@/server/controllers/announcements.js';
|
||||
import { authMisskeyController } from '@/server/controllers/auth-misskey.js';
|
||||
import { callbackLegacyAuthController } from '@/server/controllers/callback-legacy-auth.js';
|
||||
import { callbackMiauthController } from '@/server/controllers/callback-miauth.js';
|
||||
import { frontendController } from '@/server/controllers/frontend.js';
|
||||
import { rescueController } from '@/server/controllers/rescue.js';
|
||||
import { viteController } from '@/server/controllers/vite.js';
|
||||
|
||||
export const startServer = async () => {
|
||||
const app = fastify();
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
|
||||
await app.register(fastifyView, {
|
||||
root: __dirname + '/../public/views',
|
||||
engine: { pug },
|
||||
defaultContext: { version: meta.version },
|
||||
});
|
||||
|
||||
await app.register(fastifyTRPCPlugin, {
|
||||
prefix: '/api',
|
||||
trpcOptions: {
|
||||
router: appRouter,
|
||||
createContext,
|
||||
},
|
||||
});
|
||||
|
||||
app.get('/login', authMisskeyController);
|
||||
app.get('/miauth', callbackMiauthController);
|
||||
app.get('/legacy-auth', callbackLegacyAuthController);
|
||||
app.get('/announcements/:id', announcementsController);
|
||||
app.get('/__rescue__', rescueController);
|
||||
app.get('/vite/*', viteController);
|
||||
app.get('/*', frontendController);
|
||||
|
||||
// アラートを発火させるキュー
|
||||
queues.holicCronQueue.add('cron', null, {
|
||||
jobId: 'cron',
|
||||
repeat: {
|
||||
pattern: '0 0 0 * * *',
|
||||
},
|
||||
});
|
||||
|
||||
await app.listen({
|
||||
port: config.port || 3000,
|
||||
});
|
||||
};
|
||||
|
7
packages/backend/src/server/utils/die.ts
Normal file
7
packages/backend/src/server/utils/die.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import type { ErrorCode } from 'tools-shared/dist/types/error-code.js';
|
||||
|
||||
export const die = async (reply: FastifyReply, error: ErrorCode = 'other', status = 400): Promise<void> => {
|
||||
reply.statusCode = status;
|
||||
return await reply.view('frontend', { error });
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import rndstr from 'rndstr';
|
||||
|
||||
import type { UsedToken } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
/**
|
||||
* Misskey Tools API アクセストークンを生成します
|
||||
*/
|
||||
export const generateAccessToken = async (): Promise<string> => {
|
||||
let used: UsedToken | null = null;
|
||||
let token: string;
|
||||
do {
|
||||
token = rndstr(32);
|
||||
used = await prisma.usedToken.findUnique({ where: { token } });
|
||||
} while (used != null);
|
||||
return token;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
export const getAccountByAccessToken = async (accessToken: string) => {
|
||||
return await prisma.account.findUnique({
|
||||
where: { accessToken },
|
||||
});
|
||||
};
|
52
packages/backend/src/services/holic/format.ts
Normal file
52
packages/backend/src/services/holic/format.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { defaultTemplate } from 'tools-shared/dist/const.js';
|
||||
import { createGacha } from 'tools-shared/dist/functions/create-gacha.js';
|
||||
|
||||
import type { HolicAccount, HolicRecord, MisskeySession } from '@prisma/client';
|
||||
|
||||
import { config } from '@/config.js';
|
||||
|
||||
export type VariableParameter = {
|
||||
today: HolicRecord;
|
||||
yesterday: HolicRecord;
|
||||
session: MisskeySession;
|
||||
account: HolicAccount;
|
||||
};
|
||||
|
||||
/**
|
||||
* 埋め込み変数の型
|
||||
*/
|
||||
export type Variable = string | ((p: VariableParameter) => string);
|
||||
|
||||
/**
|
||||
* 埋め込み可能な変数のリスト
|
||||
*/
|
||||
export const variables: Record<string, Variable> = {
|
||||
notesCount: ({ today }) => String(today.notesCount),
|
||||
followingCount: ({ today }) => String(today.followingCount),
|
||||
followersCount: ({ today }) => String(today.followersCount),
|
||||
notesDelta: ({ today, yesterday }) => String(today.notesCount - yesterday.notesCount),
|
||||
followingDelta: ({ today, yesterday }) => String(today.followingCount - yesterday.followingCount),
|
||||
followersDelta: ({ today, yesterday }) => String(today.followersCount - yesterday.followersCount),
|
||||
rating: ({ today }) => String(today.rating),
|
||||
ratingDelta: ({ today, yesterday }) => String(today.rating - yesterday.rating),
|
||||
url: config.url,
|
||||
username: ({ session }) => String(session.host),
|
||||
host: ({ session }) => String(session.host),
|
||||
gacha: () => createGacha(),
|
||||
};
|
||||
|
||||
const variableRegex = /\{([a-zA-Z0-9_]+?)}/g;
|
||||
|
||||
/**
|
||||
* アラート用のテキストを生成する
|
||||
* @param record ミス廃レコード
|
||||
* @param account ミス廃アカウント
|
||||
* @returns 生成したテキスト
|
||||
*/
|
||||
export const format = (p: VariableParameter): string => {
|
||||
const template = p.account.template || defaultTemplate;
|
||||
return template.replace(variableRegex, (m, name) => {
|
||||
const v = variables[name];
|
||||
return !v ? m : typeof v === 'function' ? v(p) : v;
|
||||
}) + '\n\n#misskeholic';
|
||||
};
|
20
packages/backend/src/services/holic/get-ranking.ts
Normal file
20
packages/backend/src/services/holic/get-ranking.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
/**
|
||||
* ミス廃ランキングを取得する
|
||||
* @param limit 取得する件数
|
||||
* @returns ミス廃ランキング
|
||||
*/
|
||||
export const getRanking = async (limit?: number | null): Promise<User[]> => {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
bannedFromRanking: false,
|
||||
},
|
||||
orderBy: {
|
||||
rating: 'desc',
|
||||
},
|
||||
take: limit ?? undefined,
|
||||
});
|
||||
};
|
10
packages/backend/src/services/holic/run-holic-immediately.ts
Normal file
10
packages/backend/src/services/holic/run-holic-immediately.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { queues } from '@/queue/index.js';
|
||||
|
||||
/**
|
||||
* 今すぐミス廃アラートを実行するようキューイングします。
|
||||
*/
|
||||
export const runHolicImmediately = () => {
|
||||
queues.holicCronQueue.add('immediately', null, {
|
||||
jobId: 'immediately',
|
||||
});
|
||||
};
|
29
packages/backend/src/services/note-scheduler/create.ts
Normal file
29
packages/backend/src/services/note-scheduler/create.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import type { MisskeySession, ScheduledNote } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { queues } from '@/queue';
|
||||
|
||||
export const createScheduledNote = async (session: MisskeySession, note: Omit<ScheduledNote, 'id' | 'date' | 'misskeySessionId'>, timestamp: Date): Promise<ScheduledNote> => {
|
||||
const data = await prisma.scheduledNote.create({
|
||||
data: {
|
||||
date: timestamp,
|
||||
misskeySessionId: session.id,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
visibility: note.visibility,
|
||||
localOnly: note.localOnly,
|
||||
visibleUserIds: note.visibleUserIds,
|
||||
},
|
||||
});
|
||||
const delay = dayjs(timestamp).diff(dayjs());
|
||||
console.log('note after ' + (delay / 1000) + 's');
|
||||
await queues.noteSchedulerQueue.add('noteScheduler', { id: data.id }, {
|
||||
delay,
|
||||
attempts: 5,
|
||||
backoff: 10000,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
20
packages/backend/src/services/note-scheduler/delete.ts
Normal file
20
packages/backend/src/services/note-scheduler/delete.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { Account } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { getSessionIdsByAccount } from '@/services/sessions/get-session-ids-by-account.js';
|
||||
|
||||
export const deleteScheduledNote = async (id: string, account: Account) => {
|
||||
const sessionIds = await getSessionIdsByAccount(account);
|
||||
|
||||
const note = await prisma.scheduledNote.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
misskeySessionId: true,
|
||||
},
|
||||
});
|
||||
if (note == null || !sessionIds.includes(note.misskeySessionId)) return;
|
||||
|
||||
await prisma.scheduledNote.delete({
|
||||
where: { id },
|
||||
});
|
||||
};
|
25
packages/backend/src/services/note-scheduler/get.ts
Normal file
25
packages/backend/src/services/note-scheduler/get.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { Account } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { getSessionIdsByAccount } from '@/services/sessions/get-session-ids-by-account.js';
|
||||
|
||||
export const getScheduledNotes = async (account: Account) => {
|
||||
const sessionIds = await getSessionIdsByAccount(account);
|
||||
|
||||
const notes = await prisma.scheduledNote.findMany({
|
||||
where: { misskeySessionId: { in: sessionIds } },
|
||||
orderBy: { date: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
misskeySessionId: true,
|
||||
date: true,
|
||||
text: true,
|
||||
cw: true,
|
||||
localOnly: true,
|
||||
visibility: true,
|
||||
visibleUserIds: true,
|
||||
},
|
||||
});
|
||||
|
||||
return notes;
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import { prisma } from '@/libs/prisma';
|
||||
|
||||
export const getSessionByMisskeyAcct = async (username: string, host: string) => {
|
||||
return await prisma.misskeySession.findUnique({
|
||||
where: { username_host: { username, host } },
|
||||
include: { account: true },
|
||||
});
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
export const getSessionByMisskeyToken = async (misskeyToken: string) => {
|
||||
return await prisma.misskeySession.findFirst({ where: { token: misskeyToken } });
|
||||
};
|
||||
|
@ -0,0 +1,10 @@
|
||||
import type { Account } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
export const getSessionIdsByAccount = async (account: Account) => {
|
||||
return (await prisma.misskeySession.findMany({
|
||||
where: { accountId: account.id },
|
||||
select: { id: true },
|
||||
})).map(s => s.id);
|
||||
};
|
11
packages/backend/src/services/sessions/get-session.ts
Normal file
11
packages/backend/src/services/sessions/get-session.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { Account, MisskeySession } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
export const getSession = async (sessionId: MisskeySession['id'], accountId: Account['id']) => {
|
||||
const session = await prisma.misskeySession.findUnique({ where: { id: sessionId } });
|
||||
if (!session) return null;
|
||||
if (session.accountId !== accountId) return null;
|
||||
|
||||
return session;
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import type { Account } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
|
||||
export const getSessionsByAccount = async (account: Account) => {
|
||||
return await prisma.misskeySession.findMany({
|
||||
where: { accountId: account.id },
|
||||
});
|
||||
};
|
45
packages/backend/src/services/sessions/process-login.ts
Normal file
45
packages/backend/src/services/sessions/process-login.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { currentTokenVersion } from 'tools-shared/dist/const.js';
|
||||
|
||||
import type { UserDetailed as MkUser } from 'misskey-js/built/entities.js';
|
||||
|
||||
import { prisma } from '@/libs/prisma.js';
|
||||
import { generateAccessToken } from '@/services/accounts/generate-access-token.js';
|
||||
import { getSessionByMisskeyAcct } from '@/services/sessions/get-session-by-misskey-acct';
|
||||
|
||||
/**
|
||||
* Misskey認証によるログイン処理
|
||||
* @return アクセストークン。
|
||||
*/
|
||||
export const processLogin = async (misskeyUser: MkUser, host: string, misskeyToken: string): Promise<string> => {
|
||||
const session = await getSessionByMisskeyAcct(misskeyUser.username, host);
|
||||
|
||||
if (!session) {
|
||||
// アカウントを作成する
|
||||
const accessToken = await generateAccessToken();
|
||||
await prisma.account.create({
|
||||
data: {
|
||||
accessToken,
|
||||
name: misskeyUser.username,
|
||||
misskeySessions: {
|
||||
create: {
|
||||
username: misskeyUser.username,
|
||||
host,
|
||||
token: misskeyToken,
|
||||
tokenVersion: currentTokenVersion,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Note: 少しでもデータ転送量を抑える(Prismaはcreateの後に絶対selectを実行してしまう)
|
||||
select: { id: true },
|
||||
});
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
// Misskey トークンを更新する
|
||||
await prisma.misskeySession.update({
|
||||
where: { id: session.id },
|
||||
data: { token: misskeyToken },
|
||||
});
|
||||
|
||||
return session.account.accessToken;
|
||||
};
|
2
packages/backend/src/types/acct.ts
Normal file
2
packages/backend/src/types/acct.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type Acct = `@${string}@${string}`;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user