Compare commits

...

235 Commits

Author SHA1 Message Date
ab81aeb80f
wip: get ready for new version 2023-12-16 07:57:14 +09:00
Ebise Lutica
ab8de445ca Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-18 23:06:19 +09:00
Ebise Lutica
5455ab0bcc Update Docs 2023-06-18 23:06:16 +09:00
Xeltica
ba60b42ac7 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-14 15:06:03 +09:00
Ebise Lutica
f49c0dea6a タブUIを実装 2023-06-14 13:53:11 +09:00
Xeltica
4544e75da6 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-14 08:52:10 +09:00
Ebise Lutica
05f16b5fc2 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-13 10:39:49 +09:00
Ebise Lutica
d03eeeb9f3 wip: タブコンポーネントを追加 2023-06-13 10:39:46 +09:00
Xeltica
6c93505ef2 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-11 02:07:27 +09:00
Ebise Lutica
063781478b wip: feat: ミス廃アラート 2023-06-09 16:36:31 +09:00
Ebise Lutica
720895df07 fix: フロントエンドにフォントが配送されない問題を修正 2023-06-09 16:36:17 +09:00
Ebise Lutica
064375c9c4 enhance(frontend): 初期画面にGitHubバッジを追加 2023-06-09 06:15:29 +09:00
Ebise Lutica
4cabd75eec __misshaialert → __tools 2023-06-09 06:15:05 +09:00
Ebise Lutica
5b700d1209 update deps 2023-06-09 06:14:45 +09:00
Ebise Lutica
fa25b79bed ミス廃API 2023-06-09 06:14:34 +09:00
Xeltica
f522c5bd0f Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-07 20:08:15 +09:00
Ebise Lutica
5b8d0c0e87 不要なコードを削除 2023-06-07 19:46:51 +09:00
Ebise Lutica
01cb352670 enhance(backend): 既存のDBからマイグレするスクリプトを本体から分割 2023-06-07 19:11:48 +09:00
Ebise Lutica
8c697ea8ae wip: ミス廃アラート 2023-06-07 17:54:58 +09:00
Xeltica
3b89540995 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-06-07 08:58:05 +09:00
Xeltica
1d61c7e740 キューの名前を定数化 2023-06-07 08:57:57 +09:00
Ebise Lutica
0c9c43921b 微調整 2023-06-06 18:37:27 +09:00
Xeltica
40ea641623 ジョブキューをBullMQに置き換え 2023-05-28 12:20:53 +09:00
Xeltica
0f2707de21 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-05-27 15:05:43 +09:00
Ebise Lutica
676f50086a
Merge pull request #118 from shrimpia/l10n_develop
New Crowdin updates
2023-05-26 01:49:53 +09:00
Ebise Lutica
3d63c31da3 予約投稿機能 2023-05-26 01:48:25 +09:00
Ebise Lutica
15707f3871 New translations ja-JP.json (Japanese, Kansai) 2023-05-24 15:27:13 +09:00
Ebise Lutica
be3ea5e526 New translations ja-JP.json (Suspicious Japanese) 2023-05-24 15:27:12 +09:00
Ebise Lutica
6b370f4aa1 New translations ja-JP.json (Lojban) 2023-05-24 15:27:11 +09:00
Ebise Lutica
96062b9929 New translations ja-JP.json (Esperanto) 2023-05-24 15:27:10 +09:00
Ebise Lutica
ede2d0a2a2 New translations ja-JP.json (Pirate English) 2023-05-24 15:27:09 +09:00
Ebise Lutica
033304370d New translations ja-JP.json (English, United States) 2023-05-24 15:27:08 +09:00
Ebise Lutica
53d7531cc4 New translations ja-JP.json (Malay) 2023-05-24 15:27:07 +09:00
Ebise Lutica
553577c095 New translations ja-JP.json (Hindi) 2023-05-24 15:27:06 +09:00
Ebise Lutica
0a624208dc New translations ja-JP.json (Bengali) 2023-05-24 15:27:05 +09:00
Ebise Lutica
40ca4799c1 New translations ja-JP.json (Indonesian) 2023-05-24 15:27:04 +09:00
Ebise Lutica
cc33a9c155 New translations ja-JP.json (Chinese Traditional) 2023-05-24 15:27:03 +09:00
Ebise Lutica
e301b23e09 New translations ja-JP.json (Chinese Simplified) 2023-05-24 15:27:02 +09:00
Ebise Lutica
7f0f2690ba New translations ja-JP.json (Ukrainian) 2023-05-24 15:27:01 +09:00
Ebise Lutica
0b1ebe0628 New translations ja-JP.json (Russian) 2023-05-24 15:27:00 +09:00
Ebise Lutica
d0733336e7 New translations ja-JP.json (Portuguese) 2023-05-24 15:26:59 +09:00
Ebise Lutica
a2413e3d3d New translations ja-JP.json (Korean) 2023-05-24 15:26:58 +09:00
Ebise Lutica
dc1de5f140 New translations ja-JP.json (Italian) 2023-05-24 15:26:57 +09:00
Ebise Lutica
4dfbd474ff New translations ja-JP.json (German) 2023-05-24 15:26:56 +09:00
Ebise Lutica
46e46a971c New translations ja-JP.json (Arabic) 2023-05-24 15:26:56 +09:00
Ebise Lutica
82dca8fae1 New translations ja-JP.json (Spanish) 2023-05-24 15:26:55 +09:00
Ebise Lutica
889397eb30 New translations ja-JP.json (French) 2023-05-24 15:26:54 +09:00
Xeltica
fcdb85c90f ジョブキュー実装とか 2023-05-24 15:20:56 +09:00
Xeltica
0d9466a456 eslint: import typeを強制するルールを追加 2023-05-04 11:54:27 +09:00
Xeltica
33aabd788f update deps 2023-05-02 12:08:57 +09:00
Xeltica
ea6ca5c9ca update deps 2023-05-02 12:04:08 +09:00
Xeltica
0dc86565da update deps 2023-05-02 11:56:43 +09:00
Ebise Lutica
8715986083 New translations ja-JP.json (Korean) 2023-05-01 16:23:00 +09:00
Ebise Lutica
3e54c5d62e New translations ja-JP.json (Korean) 2023-05-01 15:03:46 +09:00
Ebise Lutica
55bfaf4091 New translations ja-JP.json (Japanese, Kansai) 2023-04-30 13:56:30 +09:00
Ebise Lutica
eb9eb07435 New translations ja-JP.json (Suspicious Japanese) 2023-04-30 13:56:29 +09:00
Ebise Lutica
b352f8e8cb New translations ja-JP.json (Lojban) 2023-04-30 13:56:28 +09:00
Ebise Lutica
ed0d79dd53 New translations ja-JP.json (Esperanto) 2023-04-30 13:56:27 +09:00
Ebise Lutica
683fe9d43f New translations ja-JP.json (Pirate English) 2023-04-30 13:56:26 +09:00
Ebise Lutica
63b69b9640 New translations ja-JP.json (English, United States) 2023-04-30 13:56:25 +09:00
Ebise Lutica
2388496647 New translations ja-JP.json (Malay) 2023-04-30 13:56:24 +09:00
Ebise Lutica
daa5f2d0c7 New translations ja-JP.json (Hindi) 2023-04-30 13:56:24 +09:00
Ebise Lutica
4455987778 New translations ja-JP.json (Bengali) 2023-04-30 13:56:23 +09:00
Ebise Lutica
ea3d21c5fb New translations ja-JP.json (Indonesian) 2023-04-30 13:56:22 +09:00
Ebise Lutica
3a1b28c46e New translations ja-JP.json (Chinese Traditional) 2023-04-30 13:56:21 +09:00
Ebise Lutica
1e5dc134eb New translations ja-JP.json (Chinese Simplified) 2023-04-30 13:56:20 +09:00
Ebise Lutica
c2147a45b0 New translations ja-JP.json (Ukrainian) 2023-04-30 13:56:19 +09:00
Ebise Lutica
773c1aa7a2 New translations ja-JP.json (Russian) 2023-04-30 13:56:18 +09:00
Ebise Lutica
7d57b381f4 New translations ja-JP.json (Portuguese) 2023-04-30 13:56:18 +09:00
Ebise Lutica
e8a562dfb5 New translations ja-JP.json (Korean) 2023-04-30 13:56:17 +09:00
Ebise Lutica
aba5ba390b New translations ja-JP.json (Italian) 2023-04-30 13:56:16 +09:00
Ebise Lutica
e2b33b7530 New translations ja-JP.json (German) 2023-04-30 13:56:15 +09:00
Ebise Lutica
1419416c43 New translations ja-JP.json (Arabic) 2023-04-30 13:56:14 +09:00
Ebise Lutica
698fd59deb New translations ja-JP.json (Spanish) 2023-04-30 13:56:13 +09:00
Ebise Lutica
39d750226c New translations ja-JP.json (French) 2023-04-30 13:56:12 +09:00
Xeltica
a62c78d6d6 feat: お知らせにいいねする機能 2023-04-30 13:52:27 +09:00
Xeltica
6c85bcd9c7 chore(frontend): ボタンの調整 2023-04-30 13:50:51 +09:00
Xeltica
c3d199c6f7 feat: ユーザー名変更機能を実装 2023-04-30 13:42:46 +09:00
Xeltica
aeb8807178 enhance(frontend): jotai-trpcを廃止し、react-queryで呼ぶように 2023-04-30 13:42:27 +09:00
Xeltica
4dd0161cb3 fix(frontend): 型エラーを修正 2023-04-30 13:40:43 +09:00
Xeltica
d3931e5289 feat(frontend): About ページ 2023-04-30 12:46:26 +09:00
Xeltica
ea65239836 お知らせページのデザイン微調整 2023-04-30 12:45:42 +09:00
Xeltica
2c70325ee0 dev(frontend): i18nが型安全に 2023-04-30 12:16:37 +09:00
Xeltica
0461fa470e fix(frontend): storybookが動かないのを修正 2023-04-30 02:40:46 +09:00
Xeltica
ffdc32ba47 feat(frontend): お知らせページ
close #110
2023-04-30 02:20:09 +09:00
Xeltica
7a361ebaa0 feat(frontend): Not Foundページ 2023-04-30 02:18:56 +09:00
Xeltica
6ccc3d9aee feat(frontend): Appウィジェット 2023-04-30 02:16:25 +09:00
Xeltica
f1b31b559d fix(frontend): storybookが動かないのを修正 2023-04-30 02:15:28 +09:00
Xeltica
892139e2df chore: importに@を使うよう統一 2023-04-30 02:13:47 +09:00
Xeltica
195455ecf0 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-04-29 15:12:09 +09:00
Ebise Lutica
ad683ee39f
Merge pull request #115 from shrimpia/l10n_develop
New Crowdin updates
2023-04-29 15:11:59 +09:00
Xeltica
65e447b5ea feat(frontend): お知らせウィジェット 2023-04-29 14:05:03 +09:00
Xeltica
721adafe8f feat(backend): お知らせAPI 2023-04-29 14:04:53 +09:00
Xeltica
34b4507e14 調整 2023-04-29 14:04:30 +09:00
Xeltica
d9a9072bd0 chore(frontend): ストーリー調整 2023-04-29 14:03:52 +09:00
Xeltica
527ed4de35 wip 2023-04-28 20:57:27 +09:00
Ebise Lutica
f9afbc0200 New translations ja-JP.json (Japanese, Kansai) 2023-04-28 14:17:30 +09:00
Xeltica
8b995f2101 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-04-28 13:18:25 +09:00
Ebise Lutica
ce90fc46e3
Merge pull request #109 from shrimpia/l10n_develop
New Crowdin updates
2023-04-28 13:18:07 +09:00
Ebise Lutica
d15b7e5b74 New translations ja-JP.json (Japanese, Kansai) 2023-04-28 13:17:57 +09:00
Ebise Lutica
33350adeb5 New translations ja-JP.json (Suspicious Japanese) 2023-04-28 13:17:57 +09:00
Ebise Lutica
8096d6dd73 New translations ja-JP.json (English, United States) 2023-04-28 13:17:54 +09:00
Ebise Lutica
c84af004de New translations ja-JP.json (Ukrainian) 2023-04-28 13:17:51 +09:00
Ebise Lutica
becefcd515 New translations ja-JP.json (Russian) 2023-04-28 13:17:51 +09:00
Ebise Lutica
163f4b1bbb New translations ja-JP.json (Portuguese) 2023-04-28 13:17:50 +09:00
Ebise Lutica
ddd8ede279 New translations ja-JP.json (German) 2023-04-28 13:17:48 +09:00
Ebise Lutica
86c88a6a56 New translations ja-JP.json (Spanish) 2023-04-28 13:17:47 +09:00
Ebise Lutica
6fce1457b6 New translations ja-JP.json (French) 2023-04-28 13:17:46 +09:00
Ebise Lutica
5fab864ff7 New translations ja-JP.json (Japanese, Kansai) 2023-04-28 12:15:50 +09:00
Ebise Lutica
c5c4f94e9b New translations ja-JP.json (Suspicious Japanese) 2023-04-28 12:15:49 +09:00
Ebise Lutica
7a444abbca New translations ja-JP.json (Lojban) 2023-04-28 12:15:48 +09:00
Ebise Lutica
8d927b54bd New translations ja-JP.json (Esperanto) 2023-04-28 12:15:47 +09:00
Ebise Lutica
c7bb861ea7 New translations ja-JP.json (Pirate English) 2023-04-28 12:15:46 +09:00
Ebise Lutica
1c2802ea37 New translations ja-JP.json (English, United States) 2023-04-28 12:15:45 +09:00
Ebise Lutica
a7adb419fd New translations ja-JP.json (Malay) 2023-04-28 12:15:44 +09:00
Ebise Lutica
c6e282b4ba New translations ja-JP.json (Hindi) 2023-04-28 12:15:43 +09:00
Ebise Lutica
4eb4cddcb1 New translations ja-JP.json (Bengali) 2023-04-28 12:15:42 +09:00
Ebise Lutica
19a6992cfd New translations ja-JP.json (Indonesian) 2023-04-28 12:15:41 +09:00
Ebise Lutica
3f0eaa9508 New translations ja-JP.json (Chinese Traditional) 2023-04-28 12:15:40 +09:00
Ebise Lutica
b56e2bab38 New translations ja-JP.json (Chinese Simplified) 2023-04-28 12:15:40 +09:00
Ebise Lutica
ad6c9c99ca New translations ja-JP.json (Ukrainian) 2023-04-28 12:15:39 +09:00
Ebise Lutica
f0d57b5e0e New translations ja-JP.json (Russian) 2023-04-28 12:15:38 +09:00
Ebise Lutica
a533f29d03 New translations ja-JP.json (Portuguese) 2023-04-28 12:15:37 +09:00
Ebise Lutica
66d644a46c New translations ja-JP.json (Korean) 2023-04-28 12:15:36 +09:00
Ebise Lutica
53502f2aeb New translations ja-JP.json (Italian) 2023-04-28 12:15:35 +09:00
Ebise Lutica
9c5dcb1a9f New translations ja-JP.json (German) 2023-04-28 12:15:34 +09:00
Ebise Lutica
cb05eb0945 New translations ja-JP.json (Arabic) 2023-04-28 12:15:33 +09:00
Ebise Lutica
e1dafb9443 New translations ja-JP.json (Spanish) 2023-04-28 12:15:32 +09:00
Ebise Lutica
7997c5716d New translations ja-JP.json (French) 2023-04-28 12:15:31 +09:00
Xeltica
86759b6729 意図的に変更した文言はキーも変更 2023-04-28 11:18:36 +09:00
Ebise Lutica
48d5a11160 New translations ja-JP.json (Japanese, Kansai) 2023-04-28 11:17:04 +09:00
Ebise Lutica
1306d98e78 New translations ja-JP.json (Suspicious Japanese) 2023-04-28 11:17:03 +09:00
Ebise Lutica
7826bfde25 New translations ja-JP.json (Lojban) 2023-04-28 11:17:02 +09:00
Ebise Lutica
2c082389e0 New translations ja-JP.json (Esperanto) 2023-04-28 11:16:57 +09:00
Ebise Lutica
b208974a55 New translations ja-JP.json (Pirate English) 2023-04-28 11:16:57 +09:00
Ebise Lutica
c40bf453e2 New translations ja-JP.json (English, United States) 2023-04-28 11:16:56 +09:00
Ebise Lutica
d9864814f1 New translations ja-JP.json (Malay) 2023-04-28 11:16:55 +09:00
Ebise Lutica
d2dd5bf8dd New translations ja-JP.json (Hindi) 2023-04-28 11:16:54 +09:00
Ebise Lutica
87fe9a1ce9 New translations ja-JP.json (Bengali) 2023-04-28 11:16:53 +09:00
Ebise Lutica
cfbca022c4 New translations ja-JP.json (Indonesian) 2023-04-28 11:16:51 +09:00
Ebise Lutica
2236e82c58 New translations ja-JP.json (Chinese Traditional) 2023-04-28 11:16:50 +09:00
Ebise Lutica
808122abff New translations ja-JP.json (Chinese Simplified) 2023-04-28 11:16:49 +09:00
Ebise Lutica
0942fa8273 New translations ja-JP.json (Ukrainian) 2023-04-28 11:16:48 +09:00
Ebise Lutica
4be7561d27 New translations ja-JP.json (Russian) 2023-04-28 11:16:48 +09:00
Ebise Lutica
94d9f87f9d New translations ja-JP.json (Portuguese) 2023-04-28 11:16:47 +09:00
Ebise Lutica
bdf86160d3 New translations ja-JP.json (Korean) 2023-04-28 11:16:46 +09:00
Ebise Lutica
86c9bf02b1 New translations ja-JP.json (Italian) 2023-04-28 11:16:45 +09:00
Ebise Lutica
7a0e25bcfd New translations ja-JP.json (German) 2023-04-28 11:16:44 +09:00
Ebise Lutica
a7bf0c2231 New translations ja-JP.json (Arabic) 2023-04-28 11:16:43 +09:00
Ebise Lutica
3e4e07c3b3 New translations ja-JP.json (Spanish) 2023-04-28 11:16:42 +09:00
Ebise Lutica
19ad5eff11 New translations ja-JP.json (French) 2023-04-28 11:16:42 +09:00
Xeltica
082e5a2ac2 Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-04-28 10:15:52 +09:00
Xeltica
1b9b92fb53 locale: "Misskey Tools" を翻訳不可能に 2023-04-28 10:15:50 +09:00
Ebise Lutica
8b25f7623b New translations ja-JP.json (Japanese, Kansai) 2023-04-28 10:05:04 +09:00
Ebise Lutica
974236223a New translations ja-JP.json (Suspicious Japanese) 2023-04-28 10:05:03 +09:00
Ebise Lutica
82fee5a804 New translations ja-JP.json (Lojban) 2023-04-28 10:05:02 +09:00
Ebise Lutica
f545567ef0 New translations ja-JP.json (Esperanto) 2023-04-28 10:05:01 +09:00
Ebise Lutica
1e22ef205b New translations ja-JP.json (Pirate English) 2023-04-28 10:05:00 +09:00
Ebise Lutica
084d7f88b9 New translations ja-JP.json (English, United States) 2023-04-28 10:04:59 +09:00
Ebise Lutica
16eb74ef22 New translations ja-JP.json (Malay) 2023-04-28 10:04:58 +09:00
Ebise Lutica
5f44ff2115 New translations ja-JP.json (Hindi) 2023-04-28 10:04:57 +09:00
Ebise Lutica
dcec09832d New translations ja-JP.json (Bengali) 2023-04-28 10:04:56 +09:00
Ebise Lutica
ada62246a5 New translations ja-JP.json (Indonesian) 2023-04-28 10:04:56 +09:00
Ebise Lutica
f6c733d1a7 New translations ja-JP.json (Chinese Traditional) 2023-04-28 10:04:55 +09:00
Ebise Lutica
9a450562d6 New translations ja-JP.json (Chinese Simplified) 2023-04-28 10:04:54 +09:00
Ebise Lutica
0aa2644f92 New translations ja-JP.json (Ukrainian) 2023-04-28 10:04:53 +09:00
Ebise Lutica
e5659ef95c New translations ja-JP.json (Russian) 2023-04-28 10:04:52 +09:00
Ebise Lutica
da40d8f482 New translations ja-JP.json (Portuguese) 2023-04-28 10:04:51 +09:00
Ebise Lutica
67e0edb430 New translations ja-JP.json (Korean) 2023-04-28 10:04:50 +09:00
Ebise Lutica
81ed27e8e8 New translations ja-JP.json (Italian) 2023-04-28 10:04:49 +09:00
Ebise Lutica
98772da1c3 New translations ja-JP.json (German) 2023-04-28 10:04:48 +09:00
Ebise Lutica
2af4488194 New translations ja-JP.json (Arabic) 2023-04-28 10:04:47 +09:00
Ebise Lutica
144daf7bcb New translations ja-JP.json (Spanish) 2023-04-28 10:04:46 +09:00
Ebise Lutica
484d2b8937 New translations ja-JP.json (French) 2023-04-28 10:04:45 +09:00
Ebise Lutica
886941ee63 Update Crowdin configuration file 2023-04-28 10:04:20 +09:00
Xeltica
2994ef1467 enhance(frontend): タブレットでのダッシュボードのレイアウト改善 2023-04-28 09:51:24 +09:00
Xeltica
beb9eee94e frontend: 設定 2023-04-28 09:46:05 +09:00
Xeltica
43e4d0f2d0 wip 2023-04-28 03:35:04 +09:00
Xeltica
6deb8b4e59 アカウントメニュー 2023-04-28 02:04:31 +09:00
Xeltica
2747a0b613 wip 2023-04-27 23:46:51 +09:00
Xeltica
820c20a1b6 chore(backend): ファイル分け 2023-04-27 19:43:25 +09:00
Xeltica
06876c528d enhance(backend): DTOを導入 2023-04-27 19:41:59 +09:00
Xeltica
ac205b9a9e ログインできるようになった 2023-04-27 18:34:41 +09:00
Xeltica
c2279f4efc wip 2023-04-27 01:39:53 +09:00
Xeltica
f3f94c604d wip 2023-04-27 01:33:47 +09:00
Xeltica
8bd9501dfe wip 2023-04-26 20:34:02 +09:00
Xeltica
d5d55e86bd fix(frontend): Welcomeページを作るなど 2023-04-26 19:22:31 +09:00
Xeltica
432a492745 fix(backend): 起動しないのを修正 2023-04-26 14:34:33 +09:00
Xeltica
def7d57c76 appsとpackagesを統合 2023-04-26 12:54:20 +09:00
Xeltica
515ba17c08 wip 2023-04-26 12:38:06 +09:00
Xeltica
bc54efe79d storybook 2023-04-25 01:45:57 +09:00
Xeltica
7030c6af37 Stitchesを導入 2023-04-25 01:44:47 +09:00
Xeltica
99108a27cc lintとか 2023-04-24 12:32:59 +09:00
Xeltica
78f8cc0302 backend: 開発環境でのフロントエンドへのアセット配信をサポート 2023-04-24 12:32:19 +09:00
Xeltica
d4e4850ffb frontend: 書き直しを開始 2023-04-24 12:30:51 +09:00
Xeltica
8416ae687e lint 2023-04-22 02:36:08 +09:00
Xeltica
3233dd63b0 lintいろいろ 2023-04-21 21:48:14 +09:00
Xeltica
920cc4354b lint 2023-04-21 21:27:13 +09:00
Xeltica
96a958b867 chore(frontend): 開発時はエラーをねこちゃんに 2023-04-21 00:52:46 +09:00
Xeltica
12a9a645c9 ユーザーマイグレ機能を実装 2023-04-20 14:26:28 +09:00
Xeltica
d1ddda9b2c DB Migration 2023-04-20 03:44:18 +09:00
Xeltica
cabdba9ead DB再設計 2023-04-20 03:31:19 +09:00
Xeltica
f7be62ec37 ねこみみアジャスターが動作しない問題を修正 2023-04-19 15:02:30 +09:00
Xeltica
bc984bbd57 とりあえずエラーつぶし 2023-04-19 13:55:44 +09:00
Xeltica
2975d96967 wip 2023-04-19 13:52:58 +09:00
Xeltica
ec55fee3c6 wip 2023-04-19 10:33:49 +09:00
Xeltica
d73c8bd90d wip 2023-04-19 02:03:29 +09:00
Xeltica
68c154e6b8 chore(backend): コード整理 2023-04-19 01:58:19 +09:00
Xeltica
2f27935ebd chore(backend): ファイル名整理 2023-04-19 01:51:27 +09:00
Xeltica
eaaf7616ce chore(backend): @/server/router.ts を分割して整理した 2023-04-19 01:47:42 +09:00
Xeltica
aeef88b79e improve(frontend): エラー潰し 2023-04-19 01:19:03 +09:00
Xeltica
62580f019e wip: フロントエンドへのtRPC導入 2023-04-19 00:52:54 +09:00
Xeltica
e90338b49c tRPCを導入 2023-04-19 00:52:33 +09:00
Xeltica
d8f8a7a99a Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-04-17 02:55:18 +09:00
Ebise Lutica
e75bea6126 wip: jotai 2023-04-15 20:59:23 +09:00
Xeltica
73275298a8 close #100 2023-04-15 18:40:15 +09:00
Xeltica
8b8d5ad752 tRPCを導入 2023-04-15 17:22:00 +09:00
Xeltica
a5d674310f Merge branch 'develop' of https://github.com/shrimpia/misskey-tools into develop 2023-04-15 15:17:27 +09:00
Ebise Lutica
9c91dfa96e wip 2023-04-15 14:16:11 +09:00
Ebise Lutica
55b5b9e4a6 不要なパッケージを削除 2023-04-05 02:54:01 +09:00
Ebise Lutica
cb7829abb3 リネーム 2023-04-03 18:23:21 +09:00
Ebise Lutica
9fa2471714 die.tsをserver配下に移動 2023-04-03 17:38:31 +09:00
Ebise Lutica
f6eacca318 バックエンドの整理 2023-04-03 17:34:19 +09:00
Ebise Lutica
88858f70dc 言語ファイルの更新 2023-04-03 05:37:34 +09:00
Ebise Lutica
3f5ea75b85 細かい調整 2023-04-03 05:37:30 +09:00
Ebise Lutica
26c2f6beb6 wip: 開発環境でフロントエンドが動作するように 2023-04-03 05:05:36 +09:00
Ebise Lutica
51879522d8 wip: monorepo化 2023-04-03 04:38:26 +09:00
Ebise Lutica
95d763713e wip: monorepo 2023-03-30 00:50:18 +09:00
Ebise Lutica
f0c853e556 calculate-all-rating.ts を廃止 2023-03-29 23:37:14 +09:00
Ebise Lutica
229c67bb96 ファイル分割 2023-03-29 23:34:54 +09:00
Ebise Lutica
ca0f0e55c2 4.0.0-dev 2023-03-29 23:17:31 +09:00
Ebise Lutica
1d212f6a9a
Merge pull request #92 from shrimpia/prisma
migrate TypeORM to Prisma
2023-03-29 21:04:06 +09:00
Ebise Lutica
f21d0459a9 TypeORMを削除 2023-03-29 20:53:36 +09:00
Ebise Lutica
a104afa852 migrateコマンドを移行 2023-03-29 20:24:19 +09:00
Ebise Lutica
9883398c56 prismaへ移行 2023-03-29 19:43:01 +09:00
377 changed files with 21416 additions and 14134 deletions

View File

@ -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

View File

@ -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
View File

@ -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
View 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>

View File

@ -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>

View File

@ -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
.npmrc Normal file
View File

@ -0,0 +1 @@
auto-install-peers = true

View File

@ -1,2 +0,0 @@
yarnPath: .yarn/releases/yarn-1.22.19.cjs
nodeLinker: node-modules

25
CONTRIBUTING.md Normal file
View 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 が発生します。

View File

@ -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)

View File

@ -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 }));

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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"');
}
}

View File

@ -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');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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"');
}
}

View File

@ -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)');
}
}

View File

@ -1,10 +0,0 @@
{
"watch": [
"src"
],
"ignore": [
"src/frontend/*"
],
"ext": "ts,tsx,pug,scss",
"exec": "run-s build:backend start"
}

View File

@ -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'
}
};

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
module.exports = {
extends: ['tools']
}

View File

@ -0,0 +1 @@
DATABASE_URL="postgresql://user:pass@localhost:5432/tools?schema=public"

View 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
}
}

View 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");

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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");

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "account" ADD COLUMN "is_admin" BOOLEAN NOT NULL DEFAULT false;

View File

@ -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;

View File

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "scheduled_note_misskey_session_id_key";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "scheduled_note" ALTER COLUMN "date" SET DATA TYPE TIMESTAMP;

View 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"

View 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")
}

View File

@ -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')

View File

@ -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());
})();

View 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}`);
};

View 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;
};

View File

@ -0,0 +1 @@
export { v4 as uuid } from 'uuid';

View File

@ -0,0 +1,3 @@
import MarkdownIt from 'markdown-it';
export const markdown = new MarkdownIt();

View 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;
};

View File

@ -0,0 +1,8 @@
import { PrismaClient } from '@prisma/client';
/**
* Prisma ORMクライアント
*/
export const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});

View 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');

View 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,
});

View 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 });

View 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 });

View 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 });

View 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,
};

View 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 });

View 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.');
};

View 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,
});

View 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>;

View 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,
});

View 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>;

View 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,
});

View 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,
});

View 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;

View 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);

View 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);

View 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 },
});
}),
});

View 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;
}),
});

View 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);
}),
});

View 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,
})),
});

View 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);
}),
});

View 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;

View File

@ -0,0 +1,2 @@
export const sessionHostCache: Record<string, string> = {};
export const tokenSecretCache: Record<string, string> = {};

View 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,
} : {});
};

View 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');
}
};

View File

@ -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 });
};

View 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 });
};

View File

@ -0,0 +1,8 @@
import type { RouteHandler } from 'fastify';
/**
*
*/
export const frontendController: RouteHandler = async (_, reply) => {
await reply.view('frontend');
};

View File

@ -0,0 +1,8 @@
import type { RouteHandler } from 'fastify';
/**
* localStorageを初期化するレスキューページを配信します
*/
export const rescueController: RouteHandler = async (_, reply) => {
await reply.view('rescue');
};

View 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);
};

View 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,
});
};

View 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 });
};

View File

@ -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;
};

View File

@ -0,0 +1,7 @@
import { prisma } from '@/libs/prisma.js';
export const getAccountByAccessToken = async (accessToken: string) => {
return await prisma.account.findUnique({
where: { accessToken },
});
};

View 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';
};

View 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,
});
};

View File

@ -0,0 +1,10 @@
import { queues } from '@/queue/index.js';
/**
*
*/
export const runHolicImmediately = () => {
queues.holicCronQueue.add('immediately', null, {
jobId: 'immediately',
});
};

View 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;
};

View 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 },
});
};

View 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;
};

View File

@ -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 },
});
};

View File

@ -0,0 +1,6 @@
import { prisma } from '@/libs/prisma.js';
export const getSessionByMisskeyToken = async (misskeyToken: string) => {
return await prisma.misskeySession.findFirst({ where: { token: misskeyToken } });
};

View File

@ -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);
};

View 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;
};

View File

@ -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 },
});
};

View 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;
};

View 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